@otto-code/brain 0.8.12 → 0.8.13
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 +2 -2
- package/dist/commands/calibrate.js +8 -8
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.js +1 -1
- package/dist/config/profile-edit.js +16 -16
- package/dist/config/profiles.d.ts +19 -3
- package/dist/config/profiles.js +36 -4
- package/dist/config/schema.d.ts +8 -0
- package/dist/config/schema.js +7 -3
- package/dist/ops/archive.d.ts +14 -1
- package/dist/ops/archive.js +9 -5
- package/dist/ops/results.d.ts +21 -1
- package/dist/ops/results.js +10 -5
- package/dist/service/host-api.d.ts +7 -4
- package/dist/service/host-api.js +31 -16
- package/dist/service/process-pool.d.ts +45 -0
- package/dist/service/process-pool.js +271 -0
- package/dist/service/router.d.ts +7 -4
- package/dist/service/router.js +60 -32
- package/dist/service/scheduler.d.ts +26 -10
- package/dist/service/scheduler.js +14 -1
- package/dist/service/serve.js +107 -58
- package/dist/service/status-events.js +1 -0
- package/dist/service/supervisor.js +2 -2
- package/dist/tui/app.js +18 -9
- package/package.json +1 -1
package/dist/service/serve.js
CHANGED
|
@@ -13,7 +13,7 @@ import { execFile, spawn } from "node:child_process";
|
|
|
13
13
|
import { randomUUID } from "node:crypto";
|
|
14
14
|
import http from "node:http";
|
|
15
15
|
import https from "node:https";
|
|
16
|
-
import {
|
|
16
|
+
import { getCalibrationForBudget, forModel, loadPersistedConfig, loadProfilesStore, put, putCalibration, saveBrainConfig, saveProfilesStore, } from "../config/index.js";
|
|
17
17
|
import { resolveBrainPaths } from "../config/paths.js";
|
|
18
18
|
import { query as queryGpu } from "../gpu.js";
|
|
19
19
|
import { managedModelsDir, pickAutoModel, pickModel, scanModels } from "../models/index.js";
|
|
@@ -31,6 +31,7 @@ import { createHostApi } from "./host-api.js";
|
|
|
31
31
|
import { errorMessage } from "./http-util.js";
|
|
32
32
|
import { createRouter, createSlotEraser, Telemetry } from "./router.js";
|
|
33
33
|
import { Scheduler } from "./scheduler.js";
|
|
34
|
+
import { ModelProcessPool } from "./process-pool.js";
|
|
34
35
|
import { BrainLogPublisher, BrainStatusPublisher } from "./status-events.js";
|
|
35
36
|
import { Supervisor } from "./supervisor.js";
|
|
36
37
|
import * as tailscale from "./tailscale.js";
|
|
@@ -171,9 +172,9 @@ class ServiceJobRunner {
|
|
|
171
172
|
job.queuePosition = this.scheduler.stats().queued + 1;
|
|
172
173
|
job.message = `Queued for ${model.displayName}`;
|
|
173
174
|
void this.scheduler
|
|
174
|
-
.submit(model, () => controller.signal.aborted
|
|
175
|
+
.submit(model, (supervisor) => controller.signal.aborted
|
|
175
176
|
? Promise.reject(new Error("Operation canceled."))
|
|
176
|
-
: this.runResidentJob(kind, model.id, {
|
|
177
|
+
: this.runResidentJob(supervisor, kind, model.id, {
|
|
177
178
|
message: (value) => {
|
|
178
179
|
if (job.status === "running")
|
|
179
180
|
job.message = value.slice(-1000);
|
|
@@ -469,7 +470,7 @@ export async function startService({ config, modelNeedle, env = process.env, onL
|
|
|
469
470
|
const fit = vram.fitToBudget({
|
|
470
471
|
model,
|
|
471
472
|
profile,
|
|
472
|
-
calibration:
|
|
473
|
+
calibration: getCalibrationForBudget(store, model, profile),
|
|
473
474
|
totalVramBytes: gpu.totalBytes,
|
|
474
475
|
});
|
|
475
476
|
if (!fit.adjusted && !fit.budget.fits) {
|
|
@@ -495,41 +496,43 @@ export async function startService({ config, modelNeedle, env = process.env, onL
|
|
|
495
496
|
});
|
|
496
497
|
supervisor.on("log", (line) => log("server", line));
|
|
497
498
|
supervisor.on("crashed", (error) => log("model", `FATAL ${error}`));
|
|
498
|
-
|
|
499
|
-
// config path (POST /__host/config) calls loadModel directly. Chaining here
|
|
500
|
-
// guarantees two switches (e.g. a config write racing a request-driven switch)
|
|
501
|
-
// can never overlap two supervisor.start() calls, whichever caller triggers them.
|
|
502
|
-
let modelSwitchChain = Promise.resolve();
|
|
503
|
-
const loadModelUnsafe = async (target) => {
|
|
499
|
+
const loadModelInto = async (resident, target, reservedElsewhereBytes) => {
|
|
504
500
|
// A runtime can be installed from the Library tab after this service starts.
|
|
505
501
|
// Resolve it at load time so the user does not have to restart the brain.
|
|
506
|
-
|
|
507
|
-
if (!
|
|
502
|
+
resident.runtime = resolveRuntime(config, env);
|
|
503
|
+
if (!resident.runtime) {
|
|
508
504
|
throw new Error("no llama.cpp runtime available; install one from the Library tab");
|
|
509
505
|
}
|
|
510
506
|
const gpuInfo = await queryGpu();
|
|
511
507
|
let fitProfile = forModel(store, target, config.defaults);
|
|
508
|
+
let reservationBytes = 0;
|
|
512
509
|
if (gpuInfo) {
|
|
513
510
|
const fit = vram.fitToBudget({
|
|
514
511
|
model: target,
|
|
515
512
|
profile: fitProfile,
|
|
516
|
-
calibration:
|
|
517
|
-
|
|
513
|
+
calibration: getCalibrationForBudget(store, target, fitProfile),
|
|
514
|
+
// Every resident process keeps its complete budget reserved. Fit this
|
|
515
|
+
// process against the capacity left after those independent allocations.
|
|
516
|
+
totalVramBytes: Math.max(0, gpuInfo.totalBytes - reservedElsewhereBytes),
|
|
518
517
|
});
|
|
519
518
|
if (!fit.adjusted && !fit.budget.fits)
|
|
520
519
|
throw new Error(fit.reason ?? "does not fit");
|
|
521
520
|
fitProfile = fit.profile;
|
|
521
|
+
reservationBytes = fit.budget.totalBytes;
|
|
522
522
|
}
|
|
523
|
-
await
|
|
523
|
+
await resident.start(target, fitProfile);
|
|
524
524
|
delete store.pendingReloadModelIds[target.id];
|
|
525
525
|
store.lastModelId = target.id;
|
|
526
526
|
saveProfilesStore(store, paths);
|
|
527
|
+
return reservationBytes;
|
|
527
528
|
};
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
529
|
+
let processPool = null;
|
|
530
|
+
const loadModel = async (target) => {
|
|
531
|
+
if (processPool) {
|
|
532
|
+
await processPool.preload(target);
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
await loadModelInto(supervisor, target, 0);
|
|
533
536
|
};
|
|
534
537
|
// Apply an editable config patch from POST /__host/config: mutate the live
|
|
535
538
|
// config (so the lock/default getters and future starts see it), persist it to
|
|
@@ -555,15 +558,42 @@ export async function startService({ config, modelNeedle, env = process.env, onL
|
|
|
555
558
|
throw new Error("lockModel must be a boolean");
|
|
556
559
|
config.lockModel = p.lockModel;
|
|
557
560
|
}
|
|
561
|
+
if ("maxLoadedModels" in p) {
|
|
562
|
+
const next = p.maxLoadedModels;
|
|
563
|
+
if (!Number.isInteger(next) || next < 1 || next > 16) {
|
|
564
|
+
throw new Error("maxLoadedModels must be an integer from 1 to 16");
|
|
565
|
+
}
|
|
566
|
+
config.maxLoadedModels = next;
|
|
567
|
+
}
|
|
568
|
+
if ("lockedModels" in p) {
|
|
569
|
+
const next = p.lockedModels;
|
|
570
|
+
if (!Array.isArray(next) || !next.every((value) => typeof value === "string")) {
|
|
571
|
+
throw new Error("lockedModels must be an array of model ids");
|
|
572
|
+
}
|
|
573
|
+
config.lockedModels = [...new Set(next)].slice(0, config.maxLoadedModels);
|
|
574
|
+
}
|
|
575
|
+
if (config.lockedModels.length > config.maxLoadedModels) {
|
|
576
|
+
config.lockedModels = config.lockedModels.slice(0, config.maxLoadedModels);
|
|
577
|
+
}
|
|
558
578
|
const persisted = loadPersistedConfig(paths);
|
|
559
579
|
persisted.defaultModel = config.defaultModel;
|
|
560
580
|
persisted.lockModel = config.lockModel;
|
|
581
|
+
persisted.maxLoadedModels = config.maxLoadedModels;
|
|
582
|
+
persisted.lockedModels = config.lockedModels;
|
|
561
583
|
saveBrainConfig(persisted, paths);
|
|
584
|
+
await processPool?.configure(config.maxLoadedModels);
|
|
562
585
|
if (switchTo) {
|
|
563
586
|
const target = catalog.find((m) => m.displayName === switchTo || m.id === switchTo);
|
|
564
587
|
if (target)
|
|
565
588
|
await loadModel(target);
|
|
566
589
|
}
|
|
590
|
+
if (config.lockModel) {
|
|
591
|
+
for (const id of config.lockedModels) {
|
|
592
|
+
const target = catalog.find((candidate) => candidate.id === id || candidate.displayName === id);
|
|
593
|
+
if (target)
|
|
594
|
+
await loadModel(target);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
567
597
|
return redactConfig(config);
|
|
568
598
|
};
|
|
569
599
|
// One CPU sampler for the lifetime of the service: it reports a busy fraction
|
|
@@ -574,7 +604,7 @@ export async function startService({ config, modelNeedle, env = process.env, onL
|
|
|
574
604
|
// /__host/events. One instance, so `capabilities.events` and the stream can
|
|
575
605
|
// never disagree about whether this brain publishes.
|
|
576
606
|
const statusEvents = new BrainStatusPublisher();
|
|
577
|
-
const runResidentJob = async (kind, target, update, signal) => {
|
|
607
|
+
const runResidentJob = async (supervisor, kind, target, update, signal) => {
|
|
578
608
|
const ensureActive = () => {
|
|
579
609
|
if (signal.aborted)
|
|
580
610
|
throw new Error("Operation canceled.");
|
|
@@ -659,7 +689,7 @@ export async function startService({ config, modelNeedle, env = process.env, onL
|
|
|
659
689
|
supervisor.recordLog(`operation benchmark: resident model ready`);
|
|
660
690
|
const profile = supervisor.profile ?? forModel(store, targetModel, config.defaults);
|
|
661
691
|
const gpuInfo = await queryGpu();
|
|
662
|
-
const calibration =
|
|
692
|
+
const calibration = getCalibrationForBudget(store, targetModel, profile);
|
|
663
693
|
const fit = gpuInfo
|
|
664
694
|
? vram.fitToBudget({
|
|
665
695
|
model: targetModel,
|
|
@@ -703,47 +733,54 @@ export async function startService({ config, modelNeedle, env = process.env, onL
|
|
|
703
733
|
update.percent(100);
|
|
704
734
|
supervisor.recordLog(`operation benchmark: saved result for ${targetModel.displayName}`);
|
|
705
735
|
};
|
|
706
|
-
const
|
|
707
|
-
supervisor
|
|
708
|
-
|
|
736
|
+
const attachSupervisorLogs = (resident) => {
|
|
737
|
+
if (resident === supervisor)
|
|
738
|
+
return;
|
|
739
|
+
resident.on("log", (line) => log("server", line));
|
|
740
|
+
resident.on("crashed", (error) => log("model", `FATAL ${error}`));
|
|
741
|
+
resident.on("state", () => statusEvents.notify());
|
|
742
|
+
};
|
|
743
|
+
const createPooledSupervisor = (index) => {
|
|
744
|
+
const resident = new Supervisor({
|
|
745
|
+
runtime: resolveRuntime(config, env),
|
|
746
|
+
internalPort: supervisor.internalPort + index,
|
|
747
|
+
paths,
|
|
748
|
+
getProfilesStore: () => store,
|
|
749
|
+
logVerbosity: config.runtime.logVerbosity,
|
|
750
|
+
});
|
|
751
|
+
attachSupervisorLogs(resident);
|
|
752
|
+
return resident;
|
|
753
|
+
};
|
|
754
|
+
const createResidentScheduler = (resident, loadResidentModel, onChange) => new Scheduler({
|
|
755
|
+
supervisor: resident,
|
|
756
|
+
loadModel: loadResidentModel,
|
|
709
757
|
logger: (message) => log("api", `WARN ${message}`),
|
|
710
|
-
onChange
|
|
711
|
-
// Live admission: the engine's own /slots report is the source of truth
|
|
712
|
-
// for how many sequence slots are actually free right now. The profile's
|
|
713
|
-
// parallelSlots sizes the KV pool at launch; this keeps dispatch honest
|
|
714
|
-
// while llama-server is mid-eviction or saturated, and degrades to the
|
|
715
|
-
// static count when the sample is unavailable.
|
|
716
|
-
// Answers with the free slots NAMED, not just counted. The count gates
|
|
717
|
-
// admission; the ids let the scheduler pin each admitted completion to a
|
|
718
|
-
// distinct slot (`id_slot`), which is what lets the proxy attribute a
|
|
719
|
-
// request's stage - "thinking" above all, which llama-server cannot report -
|
|
720
|
-
// to the exact slot row the Overview panel draws. Handing back only a count
|
|
721
|
-
// (as this did before) leaves every request unpinned and the panel unable to
|
|
722
|
-
// say which slot is thinking and which is emitting tokens.
|
|
758
|
+
onChange,
|
|
723
759
|
freeSlots: async () => {
|
|
724
|
-
if (
|
|
760
|
+
if (resident.state !== "ready")
|
|
725
761
|
return null;
|
|
726
762
|
try {
|
|
727
|
-
const slots = await sampleSlots({
|
|
728
|
-
host: supervisor.host,
|
|
729
|
-
port: supervisor.internalPort,
|
|
730
|
-
});
|
|
763
|
+
const slots = await sampleSlots({ host: resident.host, port: resident.internalPort });
|
|
731
764
|
return slots ? { idle: slots.idle, ids: slots.idleSlots } : null;
|
|
732
765
|
}
|
|
733
766
|
catch {
|
|
734
767
|
return null;
|
|
735
768
|
}
|
|
736
769
|
},
|
|
737
|
-
|
|
738
|
-
// chat's KV never bleeds into the next chat's thinking. The scheduler
|
|
739
|
-
// decides the handoff from the owner map; this is the engine-side wipe on
|
|
740
|
-
// the private port, resolved once the engine acknowledges it (see
|
|
741
|
-
// Scheduler.OWNERSHIP). The router that shares this scheduler clears the
|
|
742
|
-
// owner map on the supervisor's `starting` state.
|
|
743
|
-
eraseSlot: createSlotEraser(supervisor.host, supervisor.internalPort),
|
|
770
|
+
eraseSlot: createSlotEraser(resident.host, resident.internalPort),
|
|
744
771
|
});
|
|
772
|
+
const scheduler = new ModelProcessPool({
|
|
773
|
+
initialSupervisor: supervisor,
|
|
774
|
+
maxModels: config.maxLoadedModels,
|
|
775
|
+
createSupervisor: createPooledSupervisor,
|
|
776
|
+
createScheduler: createResidentScheduler,
|
|
777
|
+
loadModel: loadModelInto,
|
|
778
|
+
logger: (message) => log("model", message),
|
|
779
|
+
onChange: () => statusEvents.notify(),
|
|
780
|
+
});
|
|
781
|
+
processPool = scheduler;
|
|
745
782
|
const jobs = new ServiceJobRunner(rescanCatalog, runResidentJob, scheduler, (target) => {
|
|
746
|
-
const modelId = target ??
|
|
783
|
+
const modelId = target ?? scheduler.residentSupervisors()[0]?.model?.id ?? store.lastModelId ?? null;
|
|
747
784
|
return modelId
|
|
748
785
|
? (catalog.find((candidate) => candidate.id === modelId || candidate.displayName === modelId) ?? null)
|
|
749
786
|
: null;
|
|
@@ -768,6 +805,7 @@ export async function startService({ config, modelNeedle, env = process.env, onL
|
|
|
768
805
|
}
|
|
769
806
|
},
|
|
770
807
|
loadModel,
|
|
808
|
+
unloadModels: () => scheduler.unload(),
|
|
771
809
|
scheduler,
|
|
772
810
|
// The same gate as POST /__host/config. Deleting someone's model files over
|
|
773
811
|
// the network is strictly more dangerous than changing their default model,
|
|
@@ -796,6 +834,7 @@ export async function startService({ config, modelNeedle, env = process.env, onL
|
|
|
796
834
|
getEvals: collectEvals,
|
|
797
835
|
getLockModel: () => config.lockModel,
|
|
798
836
|
getDefaultModel: () => config.defaultModel,
|
|
837
|
+
getLockedModels: () => config.lockedModels,
|
|
799
838
|
applyConfigPatch,
|
|
800
839
|
getAllowConfigWrite: allowWrite,
|
|
801
840
|
hostApi,
|
|
@@ -846,11 +885,17 @@ export async function startService({ config, modelNeedle, env = process.env, onL
|
|
|
846
885
|
throw error;
|
|
847
886
|
}
|
|
848
887
|
certManager?.start();
|
|
849
|
-
if (
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
888
|
+
if (runtime && config.lockModel && config.lockedModels.length > 0) {
|
|
889
|
+
for (const lockedId of config.lockedModels.slice(0, config.maxLoadedModels)) {
|
|
890
|
+
const locked = catalog.find((candidate) => candidate.id === lockedId || candidate.displayName === lockedId);
|
|
891
|
+
if (locked)
|
|
892
|
+
await scheduler.preload(locked);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
else if (model && profile && runtime) {
|
|
896
|
+
// Default model remains one auto-load. Additional process slots stay empty
|
|
897
|
+
// until a request names another model.
|
|
898
|
+
await scheduler.preload(model);
|
|
854
899
|
}
|
|
855
900
|
else if (!runtime) {
|
|
856
901
|
log("server", "ready: no llama.cpp runtime installed; use the Library tab to download one");
|
|
@@ -866,11 +911,15 @@ export async function startService({ config, modelNeedle, env = process.env, onL
|
|
|
866
911
|
secure: Boolean(tlsOptions),
|
|
867
912
|
displayHost,
|
|
868
913
|
}, env);
|
|
869
|
-
log("server", `ready: ${
|
|
914
|
+
log("server", `ready: ${scheduler
|
|
915
|
+
.residentSupervisors()
|
|
916
|
+
.map((resident) => resident.model?.displayName)
|
|
917
|
+
.filter(Boolean)
|
|
918
|
+
.join(", ") || "no model loaded"} on ${bindHost}:${port}; run log ${runLog.path}`);
|
|
870
919
|
const stop = async () => {
|
|
871
920
|
certManager?.stop();
|
|
872
921
|
log("server", "Brain service stopping");
|
|
873
|
-
await
|
|
922
|
+
await scheduler.stop();
|
|
874
923
|
// Publish the terminal outcome before closing the SSE responses below.
|
|
875
924
|
// Once the listener is closed, the daemon can still report the child exit,
|
|
876
925
|
// but it cannot receive this service-owned, durable session-log entry.
|
|
@@ -97,6 +97,7 @@ export function statusChangeKey(snapshot) {
|
|
|
97
97
|
state: snapshot.state ?? null,
|
|
98
98
|
model: snapshot.model ?? null,
|
|
99
99
|
modelId: snapshot.modelId ?? null,
|
|
100
|
+
residents: snapshot.residents ?? null,
|
|
100
101
|
pid: snapshot.pid ?? null,
|
|
101
102
|
vramBytes: snapshot.vramBytes ?? null,
|
|
102
103
|
loadSeconds: snapshot.loadSeconds ?? null,
|
|
@@ -11,7 +11,7 @@ import { spawn } from "node:child_process";
|
|
|
11
11
|
import { EventEmitter } from "node:events";
|
|
12
12
|
import { buildArgs, buildEnv, formatCommand } from "../runtime/index.js";
|
|
13
13
|
import { resolveHostingProfileForLaunch } from "../config/hosting-profiles.js";
|
|
14
|
-
import {
|
|
14
|
+
import { getCalibrationForBudget } from "../config/profiles.js";
|
|
15
15
|
import { resolveBrainPaths } from "../config/paths.js";
|
|
16
16
|
import { loadProfilesStore } from "../config/store.js";
|
|
17
17
|
import { usedBytes } from "../gpu.js";
|
|
@@ -115,7 +115,7 @@ export class Supervisor extends EventEmitter {
|
|
|
115
115
|
// The prompt-cache budget is derived from measured KV bytes/token, so the
|
|
116
116
|
// launch boundary is where it has to be resolved - nothing downstream of
|
|
117
117
|
// here can reach the calibration store.
|
|
118
|
-
|
|
118
|
+
getCalibrationForBudget(this.getProfilesStore(), model, launchProfile));
|
|
119
119
|
this.args = args;
|
|
120
120
|
this.command = formatCommand(runtime, args);
|
|
121
121
|
__classPrivateFieldGet(this, _Supervisor_instances, "m", _Supervisor_log).call(this, formatBrainLog("model", `launching: ${this.command}`));
|
package/dist/tui/app.js
CHANGED
|
@@ -246,7 +246,7 @@ export class App {
|
|
|
246
246
|
const fit = vram.fitToBudget({
|
|
247
247
|
model: target,
|
|
248
248
|
profile,
|
|
249
|
-
calibration: profiles.
|
|
249
|
+
calibration: profiles.getCalibrationForBudget(this.store, target, profile),
|
|
250
250
|
totalVramBytes: info.totalBytes,
|
|
251
251
|
});
|
|
252
252
|
if (!fit.adjusted && !fit.budget.fits)
|
|
@@ -430,7 +430,7 @@ export class App {
|
|
|
430
430
|
const profile = this.profile;
|
|
431
431
|
if (!model || !profile)
|
|
432
432
|
return null;
|
|
433
|
-
return profiles.
|
|
433
|
+
return profiles.getCalibrationForBudget(this.store, model, profile);
|
|
434
434
|
}
|
|
435
435
|
get budget() {
|
|
436
436
|
const model = this.model;
|
|
@@ -1398,7 +1398,9 @@ export class App {
|
|
|
1398
1398
|
// Only when it belongs to the model actually being benchmarked - the
|
|
1399
1399
|
// model may have been resident since before this fit was computed.
|
|
1400
1400
|
fit: this.lastFit?.modelId === model.id ? this.lastFit.fit : null,
|
|
1401
|
-
calibration: profile
|
|
1401
|
+
calibration: profile
|
|
1402
|
+
? profiles.getCalibrationForBudget(this.store, model, profile)
|
|
1403
|
+
: null,
|
|
1402
1404
|
suite: {
|
|
1403
1405
|
// The TUI runs the full static suite; only concurrency varies, and
|
|
1404
1406
|
// it tracks the profile's slot count the same way runSuite is called.
|
|
@@ -1818,8 +1820,12 @@ export class App {
|
|
|
1818
1820
|
const start = Math.max(0, Math.min(focusLine - Math.floor(rows / 2), content.length - rows));
|
|
1819
1821
|
lines = content.slice(start, start + rows);
|
|
1820
1822
|
}
|
|
1823
|
+
const stale = Boolean(this.profile?.calibrationRequired ||
|
|
1824
|
+
(this.model &&
|
|
1825
|
+
this.profile &&
|
|
1826
|
+
profiles.hasStaleCalibration(this.store, this.model, this.profile)));
|
|
1821
1827
|
return box({
|
|
1822
|
-
title: `Configuration${this.calibration ? ` ${style.brightGreen}calibrated${style.reset}` : ` ${style.yellow}not calibrated${style.reset}`}`,
|
|
1828
|
+
title: `Configuration${stale ? ` ${style.yellow}recalibrate${style.reset}` : this.calibration ? ` ${style.brightGreen}calibrated${style.reset}` : ` ${style.yellow}not calibrated${style.reset}`}`,
|
|
1823
1829
|
lines,
|
|
1824
1830
|
innerWidth,
|
|
1825
1831
|
footer: `${style.grey}←→ change · enter edit${style.reset}`,
|
|
@@ -1847,18 +1853,21 @@ export class App {
|
|
|
1847
1853
|
` = ${style.bold}${vram.formatGiB(b.totalBytes)}${style.reset}`;
|
|
1848
1854
|
const kvLabel = `kv ${(b.kvBytesPerToken / 1024).toFixed(1)} KB/token`;
|
|
1849
1855
|
const cal = this.calibration;
|
|
1856
|
+
const stale = Boolean(this.profile?.calibrationRequired ||
|
|
1857
|
+
(this.model &&
|
|
1858
|
+
this.profile &&
|
|
1859
|
+
profiles.hasStaleCalibration(this.store, this.model, this.profile)));
|
|
1850
1860
|
let sourceNote;
|
|
1851
1861
|
if (b.source === "measured") {
|
|
1852
1862
|
sourceNote = cal?.inherited
|
|
1853
1863
|
? `${style.yellow}${kvLabel} (measured on a relative - press c to calibrate this model)${style.reset}`
|
|
1854
|
-
:
|
|
1864
|
+
: stale
|
|
1865
|
+
? `${style.yellow}${kvLabel} (last measurement - press c to recalibrate)${style.reset}`
|
|
1866
|
+
: `${style.grey}${kvLabel} (measured)${style.reset}`;
|
|
1855
1867
|
}
|
|
1856
1868
|
else {
|
|
1857
|
-
const stale = this.model && this.profile
|
|
1858
|
-
? profiles.hasStaleCalibration(this.store, this.model, this.profile)
|
|
1859
|
-
: false;
|
|
1860
1869
|
const hint = stale
|
|
1861
|
-
? "
|
|
1870
|
+
? "recalibrate to refresh this value"
|
|
1862
1871
|
: "press c to calibrate; usually unlocks more context";
|
|
1863
1872
|
sourceNote = `${style.yellow}${kvLabel} (theoretical - ${hint})${style.reset}`;
|
|
1864
1873
|
}
|
package/package.json
CHANGED