@otto-code/brain 0.7.5
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/bin/otto-brain +2 -0
- package/dist/bench/context-corpus.d.ts +58 -0
- package/dist/bench/context-corpus.js +210 -0
- package/dist/bench/corpus.d.ts +31 -0
- package/dist/bench/corpus.js +379 -0
- package/dist/bench/curated-repos.d.ts +35 -0
- package/dist/bench/curated-repos.js +41 -0
- package/dist/bench/health.d.ts +50 -0
- package/dist/bench/health.js +112 -0
- package/dist/bench/index.d.ts +103 -0
- package/dist/bench/index.js +209 -0
- package/dist/bench/mine.d.ts +32 -0
- package/dist/bench/mine.js +101 -0
- package/dist/bench/repo-task.d.ts +93 -0
- package/dist/bench/repo-task.js +380 -0
- package/dist/bench/repo.d.ts +125 -0
- package/dist/bench/repo.js +195 -0
- package/dist/bench/rescore.d.ts +41 -0
- package/dist/bench/rescore.js +127 -0
- package/dist/bench/tasks.d.ts +124 -0
- package/dist/bench/tasks.js +1249 -0
- package/dist/bench/verify.d.ts +142 -0
- package/dist/bench/verify.js +350 -0
- package/dist/cli.d.ts +13 -0
- package/dist/cli.js +72 -0
- package/dist/commands/bench.d.ts +20 -0
- package/dist/commands/bench.js +222 -0
- package/dist/commands/calibrate.d.ts +19 -0
- package/dist/commands/calibrate.js +75 -0
- package/dist/commands/catalog.d.ts +32 -0
- package/dist/commands/catalog.js +48 -0
- package/dist/commands/config.d.ts +13 -0
- package/dist/commands/config.js +90 -0
- package/dist/commands/lifecycle.d.ts +29 -0
- package/dist/commands/lifecycle.js +194 -0
- package/dist/commands/pull.d.ts +22 -0
- package/dist/commands/pull.js +135 -0
- package/dist/commands/report.d.ts +10 -0
- package/dist/commands/report.js +34 -0
- package/dist/commands/rescore.d.ts +11 -0
- package/dist/commands/rescore.js +48 -0
- package/dist/commands/runtime.d.ts +21 -0
- package/dist/commands/runtime.js +62 -0
- package/dist/commands/scan.d.ts +24 -0
- package/dist/commands/scan.js +55 -0
- package/dist/commands/search.d.ts +31 -0
- package/dist/commands/search.js +135 -0
- package/dist/commands/share.d.ts +26 -0
- package/dist/commands/share.js +148 -0
- package/dist/commands/sweep.d.ts +21 -0
- package/dist/commands/sweep.js +69 -0
- package/dist/commands/ui.d.ts +9 -0
- package/dist/commands/ui.js +28 -0
- package/dist/config/env.d.ts +10 -0
- package/dist/config/env.js +48 -0
- package/dist/config/index.d.ts +9 -0
- package/dist/config/index.js +9 -0
- package/dist/config/otto-home.d.ts +2 -0
- package/dist/config/otto-home.js +24 -0
- package/dist/config/paths.d.ts +20 -0
- package/dist/config/paths.js +33 -0
- package/dist/config/private-files.d.ts +3 -0
- package/dist/config/private-files.js +32 -0
- package/dist/config/profiles.d.ts +34 -0
- package/dist/config/profiles.js +116 -0
- package/dist/config/schema.d.ts +1035 -0
- package/dist/config/schema.js +191 -0
- package/dist/config/store.d.ts +11 -0
- package/dist/config/store.js +73 -0
- package/dist/gguf.d.ts +61 -0
- package/dist/gguf.js +231 -0
- package/dist/gpu.d.ts +12 -0
- package/dist/gpu.js +49 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +13 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +5 -0
- package/dist/models/dirs.d.ts +9 -0
- package/dist/models/dirs.js +21 -0
- package/dist/models/download.d.ts +31 -0
- package/dist/models/download.js +105 -0
- package/dist/models/enrich.d.ts +36 -0
- package/dist/models/enrich.js +68 -0
- package/dist/models/hf.d.ts +62 -0
- package/dist/models/hf.js +114 -0
- package/dist/models/index.d.ts +15 -0
- package/dist/models/index.js +49 -0
- package/dist/models/manage.d.ts +27 -0
- package/dist/models/manage.js +112 -0
- package/dist/models/pick.d.ts +3 -0
- package/dist/models/pick.js +36 -0
- package/dist/models/scan.d.ts +13 -0
- package/dist/models/scan.js +185 -0
- package/dist/ops/archive.d.ts +34 -0
- package/dist/ops/archive.js +105 -0
- package/dist/ops/calibrate.d.ts +49 -0
- package/dist/ops/calibrate.js +74 -0
- package/dist/ops/report.d.ts +26 -0
- package/dist/ops/report.js +532 -0
- package/dist/ops/results.d.ts +207 -0
- package/dist/ops/results.js +250 -0
- package/dist/ops/sweep.d.ts +77 -0
- package/dist/ops/sweep.js +126 -0
- package/dist/output/index.d.ts +5 -0
- package/dist/output/index.js +5 -0
- package/dist/output/render.d.ts +4 -0
- package/dist/output/render.js +96 -0
- package/dist/output/types.d.ts +52 -0
- package/dist/output/types.js +9 -0
- package/dist/output/with-output.d.ts +3 -0
- package/dist/output/with-output.js +42 -0
- package/dist/run.d.ts +5 -0
- package/dist/run.js +19 -0
- package/dist/runtime/args.d.ts +21 -0
- package/dist/runtime/args.js +71 -0
- package/dist/runtime/index.d.ts +13 -0
- package/dist/runtime/index.js +41 -0
- package/dist/runtime/lmstudio.d.ts +10 -0
- package/dist/runtime/lmstudio.js +88 -0
- package/dist/runtime/managed.d.ts +26 -0
- package/dist/runtime/managed.js +148 -0
- package/dist/service/model-selector.d.ts +36 -0
- package/dist/service/model-selector.js +123 -0
- package/dist/service/pid-lock.d.ts +18 -0
- package/dist/service/pid-lock.js +50 -0
- package/dist/service/router.d.ts +144 -0
- package/dist/service/router.js +746 -0
- package/dist/service/scheduler.d.ts +74 -0
- package/dist/service/scheduler.js +110 -0
- package/dist/service/serve.d.ts +41 -0
- package/dist/service/serve.js +278 -0
- package/dist/service/supervisor.d.ts +67 -0
- package/dist/service/supervisor.js +222 -0
- package/dist/service/tailscale.d.ts +19 -0
- package/dist/service/tailscale.js +62 -0
- package/dist/service/tls.d.ts +77 -0
- package/dist/service/tls.js +192 -0
- package/dist/sysmon.d.ts +50 -0
- package/dist/sysmon.js +103 -0
- package/dist/tui/app.d.ts +208 -0
- package/dist/tui/app.js +1801 -0
- package/dist/tui/screen.d.ts +72 -0
- package/dist/tui/screen.js +176 -0
- package/dist/types.d.ts +76 -0
- package/dist/types.js +7 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +8 -0
- package/dist/vram.d.ts +67 -0
- package/dist/vram.js +132 -0
- package/package.json +58 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zod schemas for everything otto-brain persists. Following Otto's config
|
|
3
|
+
* conventions: a `version` literal, camelCase keys, `.strict()` on config the tool
|
|
4
|
+
* owns, `.passthrough()` on stores that must survive version skew (profiles carry
|
|
5
|
+
* measured data we never want a schema bump to silently drop).
|
|
6
|
+
*/
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
export const DEFAULT_REASONING_MESSAGE = "Enough analysis. Write the complete answer now.";
|
|
9
|
+
// --------------------------------------------------------------- profiles store
|
|
10
|
+
export const ProfileSchema = z
|
|
11
|
+
.object({
|
|
12
|
+
modelId: z.string().nullable().default(null),
|
|
13
|
+
modelPath: z.string().nullable().default(null),
|
|
14
|
+
mmprojPath: z.string().nullable().default(null),
|
|
15
|
+
contextSize: z.number(),
|
|
16
|
+
cacheTypeK: z.string().default("q8_0"),
|
|
17
|
+
cacheTypeV: z.string().default("q8_0"),
|
|
18
|
+
flashAttention: z.boolean().default(true),
|
|
19
|
+
gpuLayers: z.number().default(999),
|
|
20
|
+
vision: z.boolean().default(false),
|
|
21
|
+
reasoningBudget: z.number().default(1536),
|
|
22
|
+
reasoningBudgetMessage: z.string().default(DEFAULT_REASONING_MESSAGE),
|
|
23
|
+
parallelSlots: z.number().default(1),
|
|
24
|
+
batchSize: z.number().nullable().default(null),
|
|
25
|
+
ubatchSize: z.number().nullable().default(null),
|
|
26
|
+
extraArgs: z.array(z.string()).default([]),
|
|
27
|
+
})
|
|
28
|
+
.passthrough();
|
|
29
|
+
export const CalibrationSampleSchema = z
|
|
30
|
+
.object({
|
|
31
|
+
contextSize: z.number(),
|
|
32
|
+
deltaBytes: z.number(),
|
|
33
|
+
loadSeconds: z.number().nullable().optional(),
|
|
34
|
+
})
|
|
35
|
+
.strip();
|
|
36
|
+
export const CalibrationSchema = z
|
|
37
|
+
.object({
|
|
38
|
+
kvBytesPerToken: z.number(),
|
|
39
|
+
baseOverheadBytes: z.number(),
|
|
40
|
+
theoreticalKvBytesPerToken: z.number().nullable().optional(),
|
|
41
|
+
theoreticalRatio: z.number().nullable().optional(),
|
|
42
|
+
samples: z.array(CalibrationSampleSchema).optional(),
|
|
43
|
+
cacheTypeK: z.string().optional(),
|
|
44
|
+
cacheTypeV: z.string().optional(),
|
|
45
|
+
vision: z.boolean().optional(),
|
|
46
|
+
measuredAt: z.string().optional(),
|
|
47
|
+
measuredOn: z.string().optional(),
|
|
48
|
+
inherited: z.boolean().optional(),
|
|
49
|
+
})
|
|
50
|
+
.strip();
|
|
51
|
+
export const GeometryCalibrationSchema = z
|
|
52
|
+
.object({
|
|
53
|
+
kvBytesPerTokenPerLayer: z.number(),
|
|
54
|
+
baseOverheadBytes: z.number(),
|
|
55
|
+
measuredAt: z.string().optional(),
|
|
56
|
+
measuredOn: z.string().optional(),
|
|
57
|
+
measuredLayers: z.number().optional(),
|
|
58
|
+
})
|
|
59
|
+
.passthrough();
|
|
60
|
+
export const ProfilesStoreSchema = z
|
|
61
|
+
.object({
|
|
62
|
+
version: z.literal(1).default(1),
|
|
63
|
+
profiles: z.record(ProfileSchema).default({}),
|
|
64
|
+
calibrations: z.record(z.record(CalibrationSchema)).default({}),
|
|
65
|
+
geometryCalibrations: z.record(GeometryCalibrationSchema).default({}),
|
|
66
|
+
lastModelId: z.string().nullable().default(null),
|
|
67
|
+
})
|
|
68
|
+
.passthrough();
|
|
69
|
+
// --------------------------------------------------------------- brain config
|
|
70
|
+
export const ListenSchema = z
|
|
71
|
+
.object({
|
|
72
|
+
host: z.string().default("127.0.0.1"),
|
|
73
|
+
port: z.number().default(1234),
|
|
74
|
+
})
|
|
75
|
+
.strict();
|
|
76
|
+
/**
|
|
77
|
+
* Remote auth. `none` is only safe on a loopback bind; exposing the brain on a
|
|
78
|
+
* non-loopback host should carry a token. The service layer enforces this.
|
|
79
|
+
*/
|
|
80
|
+
export const AuthSchema = z
|
|
81
|
+
.object({
|
|
82
|
+
mode: z.enum(["none", "token"]).default("none"),
|
|
83
|
+
token: z.string().nullable().default(null),
|
|
84
|
+
})
|
|
85
|
+
.strict();
|
|
86
|
+
/**
|
|
87
|
+
* TLS termination, built into the brain so it can be exposed over HTTPS with no
|
|
88
|
+
* relay in front. Four modes:
|
|
89
|
+
* - `off` — plain HTTP (the default; loopback-only is the safe posture).
|
|
90
|
+
* - `files` — bring your own cert/key (a real cert, or one you manage).
|
|
91
|
+
* - `self-signed` — generate a local keypair on first run, cached under `certDir`.
|
|
92
|
+
* - `tailscale` — issue and auto-renew a real Let's Encrypt cert for this
|
|
93
|
+
* machine's MagicDNS name via `tailscaled` (no cert warnings on
|
|
94
|
+
* the tailnet). `hostname` is auto-detected when null.
|
|
95
|
+
* The service layer enforces that a non-loopback bind still carries auth.
|
|
96
|
+
*/
|
|
97
|
+
export const TlsSchema = z
|
|
98
|
+
.object({
|
|
99
|
+
mode: z.enum(["off", "files", "self-signed", "tailscale"]).default("off"),
|
|
100
|
+
// files mode:
|
|
101
|
+
certFile: z.string().nullable().default(null),
|
|
102
|
+
keyFile: z.string().nullable().default(null),
|
|
103
|
+
// tailscale / self-signed:
|
|
104
|
+
hostname: z.string().nullable().default(null),
|
|
105
|
+
// where issued/generated certs are cached; null => $OTTO_HOME/otto-brain/certs.
|
|
106
|
+
certDir: z.string().nullable().default(null),
|
|
107
|
+
renewBeforeDays: z.number().default(21),
|
|
108
|
+
checkIntervalMs: z.number().default(43200000),
|
|
109
|
+
tailscaleExe: z.string().nullable().default(null),
|
|
110
|
+
})
|
|
111
|
+
.strict();
|
|
112
|
+
export const RuntimeConfigSchema = z
|
|
113
|
+
.object({
|
|
114
|
+
// auto = prefer a managed runtime, fall back to LM Studio discovery.
|
|
115
|
+
source: z.enum(["auto", "managed", "lmstudio"]).default("auto"),
|
|
116
|
+
path: z.string().nullable().default(null),
|
|
117
|
+
})
|
|
118
|
+
.strict();
|
|
119
|
+
export const ProfileDefaultsSchema = z
|
|
120
|
+
.object({
|
|
121
|
+
cacheTypeK: z.string().default("q8_0"),
|
|
122
|
+
cacheTypeV: z.string().default("q8_0"),
|
|
123
|
+
flashAttention: z.boolean().default(true),
|
|
124
|
+
reasoningBudget: z.number().default(1536),
|
|
125
|
+
parallelSlots: z.number().default(1),
|
|
126
|
+
contextCap: z.number().default(225000),
|
|
127
|
+
})
|
|
128
|
+
.strict();
|
|
129
|
+
export const BrainConfigSchema = z
|
|
130
|
+
.object({
|
|
131
|
+
version: z.literal(1).default(1),
|
|
132
|
+
// Opt-in: Otto ships with the local brain off and never auto-starts it.
|
|
133
|
+
enabled: z.boolean().default(false),
|
|
134
|
+
autoStart: z.boolean().default(false),
|
|
135
|
+
listen: ListenSchema.default({}),
|
|
136
|
+
auth: AuthSchema.default({}),
|
|
137
|
+
tls: TlsSchema.default({}),
|
|
138
|
+
runtime: RuntimeConfigSchema.default({}),
|
|
139
|
+
// null => managed default ($OTTO_HOME/otto-brain/models), unioned with LM Studio.
|
|
140
|
+
modelsDir: z.string().nullable().default(null),
|
|
141
|
+
// Hugging Face access token for gated/private repo downloads. Env
|
|
142
|
+
// HF_TOKEN / HUGGING_FACE_HUB_TOKEN take precedence at read time; this is the
|
|
143
|
+
// persisted fallback so users can set it once (config set hfToken <token>).
|
|
144
|
+
hfToken: z.string().nullable().default(null),
|
|
145
|
+
defaultModel: z.string().nullable().default(null),
|
|
146
|
+
// Pin the host to a single model: serve only the default/resident model and
|
|
147
|
+
// refuse completion requests that name a different one, instead of queuing a
|
|
148
|
+
// switch. For hosts that load one model and must not thrash between clients.
|
|
149
|
+
lockModel: z.boolean().default(false),
|
|
150
|
+
// Sharing/control gates (off by default — a brain is not remotely
|
|
151
|
+
// controllable until its owner opts in). `allowRemoteConfig`: a client with
|
|
152
|
+
// the token may CHANGE config over the network (POST /__host/config), not
|
|
153
|
+
// just use/read. `allowInsecureBind`: permit a non-loopback bind with no
|
|
154
|
+
// token (an "open, trusted network" share) — otherwise the service refuses.
|
|
155
|
+
allowRemoteConfig: z.boolean().default(false),
|
|
156
|
+
allowInsecureBind: z.boolean().default(false),
|
|
157
|
+
defaults: ProfileDefaultsSchema.default({}),
|
|
158
|
+
})
|
|
159
|
+
.strict();
|
|
160
|
+
export const DEFAULT_BRAIN_CONFIG = BrainConfigSchema.parse({});
|
|
161
|
+
// --------------------------------------------------------------- download catalog
|
|
162
|
+
export const CatalogModelSchema = z
|
|
163
|
+
.object({
|
|
164
|
+
id: z.string(),
|
|
165
|
+
name: z.string(),
|
|
166
|
+
publisher: z.string().optional(),
|
|
167
|
+
hfRepo: z.string(),
|
|
168
|
+
quant: z.string(),
|
|
169
|
+
quantFile: z.string().optional(),
|
|
170
|
+
approxWeightsBytes: z.number().optional(),
|
|
171
|
+
params: z.string().optional(),
|
|
172
|
+
moe: z.boolean().optional(),
|
|
173
|
+
vision: z.boolean().optional(),
|
|
174
|
+
thinking: z.boolean().optional(),
|
|
175
|
+
contextMax: z.number().optional(),
|
|
176
|
+
useCases: z.array(z.string()).optional(),
|
|
177
|
+
tier: z.string().optional(),
|
|
178
|
+
why: z.string().optional(),
|
|
179
|
+
status: z.string().optional(),
|
|
180
|
+
})
|
|
181
|
+
.passthrough();
|
|
182
|
+
export const CatalogSchema = z
|
|
183
|
+
.object({
|
|
184
|
+
version: z.literal(1).default(1),
|
|
185
|
+
note: z.string().optional(),
|
|
186
|
+
vramBudgetBytes: z.number().optional(),
|
|
187
|
+
systemRamBytes: z.number().optional(),
|
|
188
|
+
models: z.array(CatalogModelSchema).default([]),
|
|
189
|
+
})
|
|
190
|
+
.passthrough();
|
|
191
|
+
//# sourceMappingURL=schema.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type BrainPaths } from "./paths.js";
|
|
2
|
+
import { type BrainConfig, type Catalog, type ProfilesStore } from "./schema.js";
|
|
3
|
+
export declare function loadProfilesStore(paths?: BrainPaths): ProfilesStore;
|
|
4
|
+
export declare function saveProfilesStore(store: ProfilesStore, paths?: BrainPaths): void;
|
|
5
|
+
export declare function loadCatalog(paths?: BrainPaths): Catalog;
|
|
6
|
+
/** Persisted config only — no env overrides. Used by writers. */
|
|
7
|
+
export declare function loadPersistedConfig(paths?: BrainPaths): BrainConfig;
|
|
8
|
+
/** Effective config: persisted file with env overrides layered on top. */
|
|
9
|
+
export declare function loadBrainConfig(env?: NodeJS.ProcessEnv, paths?: BrainPaths): BrainConfig;
|
|
10
|
+
export declare function saveBrainConfig(config: BrainConfig, paths?: BrainPaths): void;
|
|
11
|
+
//# sourceMappingURL=store.d.ts.map
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Load/save for otto-brain's persisted state under `$OTTO_HOME/otto-brain/`.
|
|
3
|
+
* Reads are validated through the zod schemas; writes are atomic + private. On
|
|
4
|
+
* first run, profiles and the download catalog are migrated once from the legacy
|
|
5
|
+
* repo-local `config/` seed data so an existing calibrated setup is not lost.
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { writePrivateFileAtomicSync } from "./private-files.js";
|
|
10
|
+
import { packageRoot, resolveBrainPaths } from "./paths.js";
|
|
11
|
+
import { BrainConfigSchema, CatalogSchema, ProfilesStoreSchema, } from "./schema.js";
|
|
12
|
+
import { applyEnvOverrides } from "./env.js";
|
|
13
|
+
function readJson(file, schema) {
|
|
14
|
+
if (!existsSync(file))
|
|
15
|
+
return null;
|
|
16
|
+
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
17
|
+
const result = schema.safeParse(parsed);
|
|
18
|
+
if (!result.success) {
|
|
19
|
+
throw new Error(`invalid ${path.basename(file)}: ${result.error.issues[0]?.message ?? "schema error"}`);
|
|
20
|
+
}
|
|
21
|
+
return result.data;
|
|
22
|
+
}
|
|
23
|
+
function writeJson(file, data) {
|
|
24
|
+
writePrivateFileAtomicSync(file, `${JSON.stringify(data, null, 2)}\n`);
|
|
25
|
+
}
|
|
26
|
+
// ------------------------------------------------------------------- profiles
|
|
27
|
+
export function loadProfilesStore(paths = resolveBrainPaths()) {
|
|
28
|
+
const current = readJson(paths.profilesFile, ProfilesStoreSchema);
|
|
29
|
+
if (current)
|
|
30
|
+
return current;
|
|
31
|
+
const legacy = path.join(packageRoot(), "config", "profiles.json");
|
|
32
|
+
const migrated = readJson(legacy, ProfilesStoreSchema);
|
|
33
|
+
if (migrated) {
|
|
34
|
+
writeJson(paths.profilesFile, migrated);
|
|
35
|
+
return migrated;
|
|
36
|
+
}
|
|
37
|
+
return ProfilesStoreSchema.parse({});
|
|
38
|
+
}
|
|
39
|
+
export function saveProfilesStore(store, paths = resolveBrainPaths()) {
|
|
40
|
+
writeJson(paths.profilesFile, store);
|
|
41
|
+
}
|
|
42
|
+
// -------------------------------------------------------------------- catalog
|
|
43
|
+
export function loadCatalog(paths = resolveBrainPaths()) {
|
|
44
|
+
const current = readJson(paths.catalogFile, CatalogSchema);
|
|
45
|
+
if (current)
|
|
46
|
+
return current;
|
|
47
|
+
const legacy = path.join(packageRoot(), "config", "downloads.json");
|
|
48
|
+
const migrated = readJson(legacy, CatalogSchema);
|
|
49
|
+
if (migrated) {
|
|
50
|
+
writeJson(paths.catalogFile, migrated);
|
|
51
|
+
return migrated;
|
|
52
|
+
}
|
|
53
|
+
return CatalogSchema.parse({ models: [] });
|
|
54
|
+
}
|
|
55
|
+
// --------------------------------------------------------------------- config
|
|
56
|
+
/** Persisted config only — no env overrides. Used by writers. */
|
|
57
|
+
export function loadPersistedConfig(paths = resolveBrainPaths()) {
|
|
58
|
+
const current = readJson(paths.configFile, BrainConfigSchema);
|
|
59
|
+
if (current)
|
|
60
|
+
return current;
|
|
61
|
+
const seeded = BrainConfigSchema.parse({});
|
|
62
|
+
writeJson(paths.configFile, seeded);
|
|
63
|
+
return seeded;
|
|
64
|
+
}
|
|
65
|
+
/** Effective config: persisted file with env overrides layered on top. */
|
|
66
|
+
export function loadBrainConfig(env = process.env, paths = resolveBrainPaths(env)) {
|
|
67
|
+
return applyEnvOverrides(loadPersistedConfig(paths), env);
|
|
68
|
+
}
|
|
69
|
+
export function saveBrainConfig(config, paths = resolveBrainPaths()) {
|
|
70
|
+
const validated = BrainConfigSchema.parse(config);
|
|
71
|
+
writeJson(paths.configFile, validated);
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=store.js.map
|
package/dist/gguf.d.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { ModelMetadata } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Minimal GGUF metadata reader.
|
|
4
|
+
*
|
|
5
|
+
* We only need the header key/values that govern context length, attention
|
|
6
|
+
* geometry and rope scaling - never the tensor data - so we read a bounded
|
|
7
|
+
* prefix of the file rather than mapping gigabytes.
|
|
8
|
+
*/
|
|
9
|
+
export declare const TYPE: {
|
|
10
|
+
UINT8: number;
|
|
11
|
+
INT8: number;
|
|
12
|
+
UINT16: number;
|
|
13
|
+
INT16: number;
|
|
14
|
+
UINT32: number;
|
|
15
|
+
INT32: number;
|
|
16
|
+
FLOAT32: number;
|
|
17
|
+
BOOL: number;
|
|
18
|
+
STRING: number;
|
|
19
|
+
ARRAY: number;
|
|
20
|
+
UINT64: number;
|
|
21
|
+
INT64: number;
|
|
22
|
+
FLOAT64: number;
|
|
23
|
+
};
|
|
24
|
+
interface GgufHeader {
|
|
25
|
+
version: number;
|
|
26
|
+
tensorCount: number;
|
|
27
|
+
fileSize: number;
|
|
28
|
+
meta: ModelMetadata;
|
|
29
|
+
}
|
|
30
|
+
export declare function readMetadata(file: string): GgufHeader;
|
|
31
|
+
interface GgufSummary {
|
|
32
|
+
file: string;
|
|
33
|
+
fileSize: number;
|
|
34
|
+
ggufVersion: number;
|
|
35
|
+
tensorCount: number;
|
|
36
|
+
arch: string;
|
|
37
|
+
name: string | null;
|
|
38
|
+
sizeLabel: string | null;
|
|
39
|
+
fileType: number | null;
|
|
40
|
+
contextLength: number | null;
|
|
41
|
+
blockCount: number | null;
|
|
42
|
+
embeddingLength: number | null;
|
|
43
|
+
headCount: number | null;
|
|
44
|
+
headCountKv: number | null;
|
|
45
|
+
keyLength: number | null;
|
|
46
|
+
valueLength: number | null;
|
|
47
|
+
ropeFreqBase: number | null;
|
|
48
|
+
ropeScalingType: string | null;
|
|
49
|
+
ropeScalingFactor: number | null;
|
|
50
|
+
expertCount: number | null;
|
|
51
|
+
eosTokenId: number | null;
|
|
52
|
+
isProjector: boolean;
|
|
53
|
+
/** Chat template exposes a thinking/reasoning channel. */
|
|
54
|
+
reasoning: boolean;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Pull out the fields that matter for hosting decisions.
|
|
58
|
+
*/
|
|
59
|
+
export declare function summarize(file: string): GgufSummary;
|
|
60
|
+
export {};
|
|
61
|
+
//# sourceMappingURL=gguf.d.ts.map
|
package/dist/gguf.js
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
/**
|
|
3
|
+
* Minimal GGUF metadata reader.
|
|
4
|
+
*
|
|
5
|
+
* We only need the header key/values that govern context length, attention
|
|
6
|
+
* geometry and rope scaling - never the tensor data - so we read a bounded
|
|
7
|
+
* prefix of the file rather than mapping gigabytes.
|
|
8
|
+
*/
|
|
9
|
+
export const TYPE = {
|
|
10
|
+
UINT8: 0,
|
|
11
|
+
INT8: 1,
|
|
12
|
+
UINT16: 2,
|
|
13
|
+
INT16: 3,
|
|
14
|
+
UINT32: 4,
|
|
15
|
+
INT32: 5,
|
|
16
|
+
FLOAT32: 6,
|
|
17
|
+
BOOL: 7,
|
|
18
|
+
STRING: 8,
|
|
19
|
+
ARRAY: 9,
|
|
20
|
+
UINT64: 10,
|
|
21
|
+
INT64: 11,
|
|
22
|
+
FLOAT64: 12,
|
|
23
|
+
};
|
|
24
|
+
const FIXED_WIDTH = {
|
|
25
|
+
[TYPE.UINT8]: 1,
|
|
26
|
+
[TYPE.INT8]: 1,
|
|
27
|
+
[TYPE.UINT16]: 2,
|
|
28
|
+
[TYPE.INT16]: 2,
|
|
29
|
+
[TYPE.UINT32]: 4,
|
|
30
|
+
[TYPE.INT32]: 4,
|
|
31
|
+
[TYPE.FLOAT32]: 4,
|
|
32
|
+
[TYPE.BOOL]: 1,
|
|
33
|
+
[TYPE.UINT64]: 8,
|
|
34
|
+
[TYPE.INT64]: 8,
|
|
35
|
+
[TYPE.FLOAT64]: 8,
|
|
36
|
+
};
|
|
37
|
+
// Vocabularies can hold hundreds of thousands of strings; never materialize them.
|
|
38
|
+
const MAX_ARRAY_ITEMS = 64;
|
|
39
|
+
class Cursor {
|
|
40
|
+
constructor(buffer) {
|
|
41
|
+
this.buf = buffer;
|
|
42
|
+
this.pos = 0;
|
|
43
|
+
}
|
|
44
|
+
get remaining() {
|
|
45
|
+
return this.buf.length - this.pos;
|
|
46
|
+
}
|
|
47
|
+
need(bytes) {
|
|
48
|
+
if (this.remaining < bytes) {
|
|
49
|
+
const err = new Error("GGUF header extends beyond the bytes read");
|
|
50
|
+
err.code = "GGUF_NEED_MORE";
|
|
51
|
+
throw err;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
take(bytes) {
|
|
55
|
+
this.need(bytes);
|
|
56
|
+
const slice = this.buf.subarray(this.pos, this.pos + bytes);
|
|
57
|
+
this.pos += bytes;
|
|
58
|
+
return slice;
|
|
59
|
+
}
|
|
60
|
+
scalar(type) {
|
|
61
|
+
const width = FIXED_WIDTH[type];
|
|
62
|
+
if (width === undefined)
|
|
63
|
+
throw new Error(`unsupported GGUF scalar type ${type}`);
|
|
64
|
+
this.need(width);
|
|
65
|
+
const { buf, pos } = this;
|
|
66
|
+
let value;
|
|
67
|
+
switch (type) {
|
|
68
|
+
case TYPE.UINT8:
|
|
69
|
+
value = buf.readUInt8(pos);
|
|
70
|
+
break;
|
|
71
|
+
case TYPE.INT8:
|
|
72
|
+
value = buf.readInt8(pos);
|
|
73
|
+
break;
|
|
74
|
+
case TYPE.UINT16:
|
|
75
|
+
value = buf.readUInt16LE(pos);
|
|
76
|
+
break;
|
|
77
|
+
case TYPE.INT16:
|
|
78
|
+
value = buf.readInt16LE(pos);
|
|
79
|
+
break;
|
|
80
|
+
case TYPE.UINT32:
|
|
81
|
+
value = buf.readUInt32LE(pos);
|
|
82
|
+
break;
|
|
83
|
+
case TYPE.INT32:
|
|
84
|
+
value = buf.readInt32LE(pos);
|
|
85
|
+
break;
|
|
86
|
+
case TYPE.FLOAT32:
|
|
87
|
+
value = buf.readFloatLE(pos);
|
|
88
|
+
break;
|
|
89
|
+
case TYPE.BOOL:
|
|
90
|
+
value = buf.readUInt8(pos) !== 0;
|
|
91
|
+
break;
|
|
92
|
+
case TYPE.FLOAT64:
|
|
93
|
+
value = buf.readDoubleLE(pos);
|
|
94
|
+
break;
|
|
95
|
+
case TYPE.UINT64:
|
|
96
|
+
value = Number(buf.readBigUInt64LE(pos));
|
|
97
|
+
break;
|
|
98
|
+
case TYPE.INT64:
|
|
99
|
+
value = Number(buf.readBigInt64LE(pos));
|
|
100
|
+
break;
|
|
101
|
+
default:
|
|
102
|
+
throw new Error(`unsupported GGUF scalar type ${type}`);
|
|
103
|
+
}
|
|
104
|
+
this.pos += width;
|
|
105
|
+
return value;
|
|
106
|
+
}
|
|
107
|
+
string() {
|
|
108
|
+
const length = this.scalar(TYPE.UINT64);
|
|
109
|
+
return this.take(length).toString("utf8");
|
|
110
|
+
}
|
|
111
|
+
value(type) {
|
|
112
|
+
if (type === TYPE.STRING)
|
|
113
|
+
return this.string();
|
|
114
|
+
if (type !== TYPE.ARRAY)
|
|
115
|
+
return this.scalar(type);
|
|
116
|
+
const elemType = this.scalar(TYPE.UINT32);
|
|
117
|
+
const count = this.scalar(TYPE.UINT64);
|
|
118
|
+
if (count > MAX_ARRAY_ITEMS) {
|
|
119
|
+
// Skip the payload without decoding it.
|
|
120
|
+
if (elemType === TYPE.STRING) {
|
|
121
|
+
for (let i = 0; i < count; i += 1)
|
|
122
|
+
this.string();
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
this.take(FIXED_WIDTH[elemType] * count);
|
|
126
|
+
}
|
|
127
|
+
return { skipped: true, count, elemType };
|
|
128
|
+
}
|
|
129
|
+
const items = [];
|
|
130
|
+
for (let i = 0; i < count; i += 1)
|
|
131
|
+
items.push(this.value(elemType));
|
|
132
|
+
return items;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/** Read the first `bytes` of a file (or the whole file if smaller). */
|
|
136
|
+
function readPrefix(file, bytes) {
|
|
137
|
+
const fd = fs.openSync(file, "r");
|
|
138
|
+
try {
|
|
139
|
+
const size = fs.fstatSync(fd).size;
|
|
140
|
+
const length = Math.min(bytes, size);
|
|
141
|
+
const buffer = Buffer.allocUnsafe(length);
|
|
142
|
+
fs.readSync(fd, buffer, 0, length, 0);
|
|
143
|
+
return { buffer, fileSize: size };
|
|
144
|
+
}
|
|
145
|
+
finally {
|
|
146
|
+
fs.closeSync(fd);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
export function readMetadata(file) {
|
|
150
|
+
// Grow the window if a big tokenizer pushes the header past our first read.
|
|
151
|
+
let windowBytes = 8 * 1024 * 1024;
|
|
152
|
+
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
153
|
+
const { buffer, fileSize } = readPrefix(file, windowBytes);
|
|
154
|
+
const cursor = new Cursor(buffer);
|
|
155
|
+
try {
|
|
156
|
+
const magic = cursor.take(4).toString("ascii");
|
|
157
|
+
if (magic !== "GGUF")
|
|
158
|
+
throw new Error(`${file} is not a GGUF file`);
|
|
159
|
+
const version = cursor.scalar(TYPE.UINT32);
|
|
160
|
+
const tensorCount = cursor.scalar(TYPE.UINT64);
|
|
161
|
+
const kvCount = cursor.scalar(TYPE.UINT64);
|
|
162
|
+
const meta = {};
|
|
163
|
+
for (let i = 0; i < kvCount; i += 1) {
|
|
164
|
+
const key = cursor.string();
|
|
165
|
+
const type = cursor.scalar(TYPE.UINT32);
|
|
166
|
+
meta[key] = cursor.value(type);
|
|
167
|
+
}
|
|
168
|
+
return { version, tensorCount, fileSize, meta };
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
const code = error.code;
|
|
172
|
+
if (code !== "GGUF_NEED_MORE" || windowBytes >= fileSize)
|
|
173
|
+
throw error;
|
|
174
|
+
windowBytes = Math.min(windowBytes * 4, fileSize);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
throw new Error(`could not read GGUF header from ${file}`);
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Pull out the fields that matter for hosting decisions.
|
|
181
|
+
*/
|
|
182
|
+
export function summarize(file) {
|
|
183
|
+
const { version, tensorCount, fileSize, meta } = readMetadata(file);
|
|
184
|
+
const arch = meta["general.architecture"] || "unknown";
|
|
185
|
+
const get = (suffix) => meta[`${arch}.${suffix}`];
|
|
186
|
+
const heads = get("attention.head_count");
|
|
187
|
+
const embedding = get("embedding_length");
|
|
188
|
+
let keyLength = get("attention.key_length");
|
|
189
|
+
let valueLength = get("attention.value_length");
|
|
190
|
+
if (keyLength === undefined && typeof embedding === "number" && typeof heads === "number") {
|
|
191
|
+
keyLength = embedding / heads;
|
|
192
|
+
}
|
|
193
|
+
if (valueLength === undefined)
|
|
194
|
+
valueLength = keyLength;
|
|
195
|
+
let kvHeads = get("attention.head_count_kv");
|
|
196
|
+
if (Array.isArray(kvHeads))
|
|
197
|
+
kvHeads = Math.max(...kvHeads);
|
|
198
|
+
// A reasoning model advertises its thinking channel in the chat template: a
|
|
199
|
+
// `<think>` block, a `reasoning_content` field, or an `enable_thinking`
|
|
200
|
+
// toggle. Detecting it here (rather than from the filename or the catalog)
|
|
201
|
+
// means any local model is flagged, matching LM Studio's green-brain marker.
|
|
202
|
+
const chatTemplate = typeof meta["tokenizer.chat_template"] === "string"
|
|
203
|
+
? meta["tokenizer.chat_template"]
|
|
204
|
+
: "";
|
|
205
|
+
const reasoning = /<think>|<\/think>|reasoning_content|enable_thinking/i.test(chatTemplate);
|
|
206
|
+
return {
|
|
207
|
+
file,
|
|
208
|
+
fileSize,
|
|
209
|
+
ggufVersion: version,
|
|
210
|
+
tensorCount,
|
|
211
|
+
arch,
|
|
212
|
+
name: meta["general.name"] || null,
|
|
213
|
+
sizeLabel: meta["general.size_label"] || null,
|
|
214
|
+
fileType: (meta["general.file_type"] ?? null),
|
|
215
|
+
contextLength: (get("context_length") ?? null),
|
|
216
|
+
blockCount: (get("block_count") ?? null),
|
|
217
|
+
embeddingLength: (embedding ?? null),
|
|
218
|
+
headCount: (heads ?? null),
|
|
219
|
+
headCountKv: typeof kvHeads === "number" ? kvHeads : null,
|
|
220
|
+
keyLength: typeof keyLength === "number" ? keyLength : null,
|
|
221
|
+
valueLength: typeof valueLength === "number" ? valueLength : null,
|
|
222
|
+
ropeFreqBase: (get("rope.freq_base") ?? null),
|
|
223
|
+
ropeScalingType: (get("rope.scaling.type") ?? null),
|
|
224
|
+
ropeScalingFactor: (get("rope.scaling.factor") ?? null),
|
|
225
|
+
expertCount: (get("expert_count") ?? null),
|
|
226
|
+
eosTokenId: (meta["tokenizer.ggml.eos_token_id"] ?? null),
|
|
227
|
+
isProjector: Boolean(meta["clip.has_vision_encoder"] || arch === "clip"),
|
|
228
|
+
reasoning,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
//# sourceMappingURL=gguf.js.map
|
package/dist/gpu.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { GpuInfo } from "./types.js";
|
|
2
|
+
/** Thin wrapper over nvidia-smi. Returns null when there is no NVIDIA GPU. */
|
|
3
|
+
/** GpuInfo plus the live sampling fields nvidia-smi also reports. */
|
|
4
|
+
export interface GpuQuery extends GpuInfo {
|
|
5
|
+
utilization: number;
|
|
6
|
+
temperature: number;
|
|
7
|
+
}
|
|
8
|
+
declare function query(): Promise<GpuQuery | null>;
|
|
9
|
+
/** Just the used-VRAM figure, for sampling during a load. */
|
|
10
|
+
declare function usedBytes(): Promise<number | null>;
|
|
11
|
+
export { query, usedBytes };
|
|
12
|
+
//# sourceMappingURL=gpu.d.ts.map
|
package/dist/gpu.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
function run(args, timeout = 10000) {
|
|
3
|
+
return new Promise((resolve, reject) => {
|
|
4
|
+
execFile("nvidia-smi", args, { timeout, windowsHide: true }, (error, stdout) => {
|
|
5
|
+
if (error)
|
|
6
|
+
reject(error);
|
|
7
|
+
else
|
|
8
|
+
resolve(String(stdout));
|
|
9
|
+
});
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
const MIB = 1024 * 1024;
|
|
13
|
+
async function query() {
|
|
14
|
+
try {
|
|
15
|
+
const out = await run([
|
|
16
|
+
"--query-gpu=name,memory.total,memory.used,utilization.gpu,temperature.gpu,driver_version,compute_cap",
|
|
17
|
+
"--format=csv,noheader,nounits",
|
|
18
|
+
]);
|
|
19
|
+
const line = out.trim().split(/\r?\n/)[0];
|
|
20
|
+
if (!line)
|
|
21
|
+
return null;
|
|
22
|
+
const [name, total, used, util, temp, driver, cap] = line.split(",").map((s) => s.trim());
|
|
23
|
+
return {
|
|
24
|
+
name,
|
|
25
|
+
totalBytes: Number(total) * MIB,
|
|
26
|
+
usedBytes: Number(used) * MIB,
|
|
27
|
+
freeBytes: (Number(total) - Number(used)) * MIB,
|
|
28
|
+
utilization: Number(util),
|
|
29
|
+
temperature: Number(temp),
|
|
30
|
+
driver,
|
|
31
|
+
computeCapability: cap,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** Just the used-VRAM figure, for sampling during a load. */
|
|
39
|
+
async function usedBytes() {
|
|
40
|
+
try {
|
|
41
|
+
const out = await run(["--query-gpu=memory.used", "--format=csv,noheader,nounits"]);
|
|
42
|
+
return Number(out.trim().split(/\r?\n/)[0]) * MIB;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export { query, usedBytes };
|
|
49
|
+
//# sourceMappingURL=gpu.js.map
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Library entry for @otto-code/brain. Exposes the command group (so the main
|
|
3
|
+
* `otto` CLI can `program.addCommand(createBrainCommand())`), the service, runtime
|
|
4
|
+
* resolution, model discovery, config, and shared types. The executable lives in
|
|
5
|
+
* main.ts.
|
|
6
|
+
*/
|
|
7
|
+
export { createBrainCommand, registerBrainCommands } from "./cli.js";
|
|
8
|
+
export { startService, type ServiceHandle, type StartServiceOptions } from "./service/serve.js";
|
|
9
|
+
export { readRunningService, readPidFile, writePidFile, removePidFile, type PidRecord, } from "./service/pid-lock.js";
|
|
10
|
+
export { resolveRuntime, ensureRuntime, listAllRuntimes, installManagedRuntime, } from "./runtime/index.js";
|
|
11
|
+
export { scanModels, pickModel, pullModel } from "./models/index.js";
|
|
12
|
+
export * from "./config/index.js";
|
|
13
|
+
export type { Model, Runtime, GpuInfo, ModelMetadata, ModelFeatures } from "./types.js";
|
|
14
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Library entry for @otto-code/brain. Exposes the command group (so the main
|
|
3
|
+
* `otto` CLI can `program.addCommand(createBrainCommand())`), the service, runtime
|
|
4
|
+
* resolution, model discovery, config, and shared types. The executable lives in
|
|
5
|
+
* main.ts.
|
|
6
|
+
*/
|
|
7
|
+
export { createBrainCommand, registerBrainCommands } from "./cli.js";
|
|
8
|
+
export { startService } from "./service/serve.js";
|
|
9
|
+
export { readRunningService, readPidFile, writePidFile, removePidFile, } from "./service/pid-lock.js";
|
|
10
|
+
export { resolveRuntime, ensureRuntime, listAllRuntimes, installManagedRuntime, } from "./runtime/index.js";
|
|
11
|
+
export { scanModels, pickModel, pullModel } from "./models/index.js";
|
|
12
|
+
export * from "./config/index.js";
|
|
13
|
+
//# sourceMappingURL=index.js.map
|
package/dist/main.d.ts
ADDED
package/dist/main.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { BrainConfig } from "../config/schema.js";
|
|
2
|
+
export interface ModelsDir {
|
|
3
|
+
dir: string;
|
|
4
|
+
origin: "managed" | "lmstudio";
|
|
5
|
+
}
|
|
6
|
+
export declare function resolveModelsDirs(config: BrainConfig, env?: NodeJS.ProcessEnv): ModelsDir[];
|
|
7
|
+
/** The single directory new downloads are written to. */
|
|
8
|
+
export declare function managedModelsDir(config: BrainConfig, env?: NodeJS.ProcessEnv): string;
|
|
9
|
+
//# sourceMappingURL=dirs.d.ts.map
|