@bendyline/gezel-inference 1.0.1 → 1.0.2

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.
Files changed (70) hide show
  1. package/dist/engines/native-file-manifest.json +404 -404
  2. package/dist/engines/native-manifest.d.ts +4 -4
  3. package/dist/engines/native-manifest.js +34 -34
  4. package/dist/http/openai-compat/engine-failure.d.ts +25 -0
  5. package/dist/http/openai-compat/engine-failure.js +76 -0
  6. package/dist/http/openai-compat/responses-streaming.js +2 -1
  7. package/dist/http/openai-compat/responses-translate.d.ts +1 -1
  8. package/dist/http/openai-compat/translate.d.ts +1 -1
  9. package/dist/http/routes/engines.js +35 -0
  10. package/dist/http/routes/v1-chat.js +10 -1
  11. package/dist/http/routes/v1-models.js +6 -4
  12. package/dist/http/routes/v1-responses.js +7 -1
  13. package/dist/knowledge/catalog-host.d.ts +9 -1
  14. package/dist/knowledge/catalog-host.js +2 -0
  15. package/dist/knowledge/manager.d.ts +14 -0
  16. package/dist/knowledge/manager.js +53 -6
  17. package/dist/knowledge/search-worker.d.ts +1 -1
  18. package/dist/knowledge/spatial-query.d.ts +2 -3
  19. package/dist/knowledge/spatial-query.js +12 -6
  20. package/dist/knowledge/worker-host.js +2 -0
  21. package/dist/memory/embed-core.d.ts +16 -0
  22. package/dist/memory/embed-core.js +31 -0
  23. package/dist/memory/embed-worker.d.ts +7 -3
  24. package/dist/memory/embed-worker.js +13 -4
  25. package/dist/memory/embeddings.d.ts +17 -2
  26. package/dist/memory/embeddings.js +36 -7
  27. package/dist/models/ensure.d.ts +2 -0
  28. package/dist/models/ensure.js +9 -0
  29. package/dist/perf/responsiveness.d.ts +14 -1
  30. package/dist/perf/responsiveness.js +77 -7
  31. package/dist/providers/llama-cpp/backend-health.d.ts +30 -0
  32. package/dist/providers/llama-cpp/backend-health.js +64 -0
  33. package/dist/providers/llama-cpp/backend-quarantine.d.ts +40 -0
  34. package/dist/providers/llama-cpp/backend-quarantine.js +100 -0
  35. package/dist/providers/llama-cpp/build-provider.js +141 -144
  36. package/dist/providers/llama-cpp/cache-adapter.js +2 -1
  37. package/dist/providers/llama-cpp/devices.d.ts +11 -2
  38. package/dist/providers/llama-cpp/devices.js +10 -4
  39. package/dist/providers/llama-cpp/engine-flags.d.ts +0 -18
  40. package/dist/providers/llama-cpp/engine-flags.js +12 -18
  41. package/dist/providers/llama-cpp/gguf-metadata.js +20 -3
  42. package/dist/providers/llama-cpp/gpu-startup-watch.d.ts +41 -0
  43. package/dist/providers/llama-cpp/gpu-startup-watch.js +58 -0
  44. package/dist/providers/llama-cpp/models.d.ts +6 -0
  45. package/dist/providers/llama-cpp/models.js +19 -1
  46. package/dist/providers/llama-cpp/offload-planner.d.ts +3 -18
  47. package/dist/providers/llama-cpp/offload-planner.js +3 -14
  48. package/dist/providers/llama-cpp/provider.d.ts +8 -0
  49. package/dist/providers/llama-cpp/provider.js +4 -1
  50. package/dist/providers/mlx/provider.js +3 -4
  51. package/dist/providers/mlx/python/qwen4_ple.py +9 -2
  52. package/dist/providers/native/capacity-broker.d.ts +21 -15
  53. package/dist/providers/native/capacity-broker.js +16 -10
  54. package/dist/providers/native/capacity-budget.d.ts +35 -3
  55. package/dist/providers/native/capacity-budget.js +49 -5
  56. package/dist/providers/native/device-capacity.d.ts +1 -1
  57. package/dist/providers/native/device-capacity.js +23 -5
  58. package/dist/providers/native/local-engine-runtime.js +16 -19
  59. package/dist/providers/native/measured-budget.js +2 -0
  60. package/dist/providers/native/supervisor.d.ts +25 -0
  61. package/dist/providers/native/supervisor.js +59 -7
  62. package/dist/system/amdgpu-adapters.d.ts +18 -0
  63. package/dist/system/amdgpu-adapters.js +55 -0
  64. package/dist/system/gpu-carve-out.d.ts +84 -0
  65. package/dist/system/gpu-carve-out.js +111 -0
  66. package/dist/system/memory.d.ts +17 -1
  67. package/dist/system/memory.js +94 -13
  68. package/dist/transformers-cache.d.ts +2 -0
  69. package/dist/transformers-cache.js +6 -3
  70. package/package.json +5 -5
@@ -17,6 +17,7 @@
17
17
  */
18
18
  import { execFile } from 'node:child_process';
19
19
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
20
+ import { totalmem } from 'node:os';
20
21
  import { delimiter, dirname, join } from 'node:path';
21
22
  import { promisify } from 'node:util';
22
23
  import { windowsHeadlessSpawnOptions } from '@bendyline/gezel/native';
@@ -124,20 +125,25 @@ function cachePath(home) {
124
125
  /**
125
126
  * Probe device memory by running `<binaryPath> --list-devices`, caching
126
127
  * the result. Reads the cache when present (VRAM capacity is stable per
127
- * machine); pass `force` to re-probe. Any failure (spawn error, no
128
+ * machine) and the OS still reports the RAM it was probed with; pass
129
+ * `force` to re-probe. Any failure (spawn error, no
128
130
  * cache, unparseable output) resolves to an empty device list — the
129
131
  * planner then simply does nothing and the engine's own `--fit`/`-ngl
130
- * auto` takes over. `nowIso` is injectable for deterministic tests.
132
+ * auto` takes over. `nowIso` and `systemRamBytes` are injectable for
133
+ * deterministic tests.
131
134
  */
132
135
  export async function probeLlamaDevices(opts) {
133
136
  const { binaryPath, home, force } = opts;
134
137
  const now = opts.nowIso ?? (() => new Date().toISOString());
138
+ const systemRamBytes = opts.systemRamBytes ?? totalmem();
135
139
  const path = cachePath(home);
136
140
  if (!force && existsSync(path)) {
137
141
  try {
138
142
  const cached = JSON.parse(readFileSync(path, 'utf8'));
139
- if (Array.isArray(cached.devices))
143
+ // A cache without the RAM figure predates the check and re-probes once.
144
+ if (Array.isArray(cached.devices) && cached.systemRamBytes === systemRamBytes) {
140
145
  return { ...cached, cached: true };
146
+ }
141
147
  }
142
148
  catch {
143
149
  // fall through to a fresh probe
@@ -165,7 +171,7 @@ export async function probeLlamaDevices(opts) {
165
171
  // Leave devices empty — caller treats "no devices" as "no offload target".
166
172
  return { devices, probedAt: now(), cached: false };
167
173
  }
168
- const probe = { devices, probedAt: now(), cached: false };
174
+ const probe = { devices, probedAt: now(), cached: false, systemRamBytes };
169
175
  try {
170
176
  mkdirSync(dirname(path), { recursive: true });
171
177
  writeFileSync(path, JSON.stringify(probe, null, 2), 'utf8');
@@ -126,24 +126,6 @@ export interface EngineFlagInput {
126
126
  * when the client also replays `reasoning_content`.
127
127
  */
128
128
  reasoningPreserve?: boolean | undefined;
129
- /**
130
- * The model's GGUF `general.architecture`, for the Gemma-family auto
131
- * defaults (sliding-window full cache). Undefined disables the arch
132
- * heuristic; an explicit config/manifest `swaFull` still applies.
133
- */
134
- architecture?: string | undefined;
135
- /** The resolved model id, a fallback for `architecture` (see isGemmaModel). */
136
- modelId?: string | undefined;
137
- /**
138
- * Whether resident weights plus the full SWA KV cache fit the host's
139
- * fast-memory pool (VRAM on a discrete GPU). The exact estimate honors
140
- * per-layer SWA dimensions and shared-KV layers. `false` declines the
141
- * auto-default so a low-memory device keeps the model's full context on
142
- * the windowed cache instead of spilling or OOMing. `undefined` (unknown
143
- * host/metadata) and `true` retain the compatibility default. An explicit
144
- * config/manifest `swaFull` wins regardless.
145
- */
146
- swaFullAutoFits?: boolean | undefined;
147
129
  /**
148
130
  * The llama-server build's backend (`cuda`, `vulkan`, `metal`, `cpu`).
149
131
  * Only a GPU backend can receive the GPU-resident thread default.
@@ -21,7 +21,6 @@
21
21
  * dependency; the real `GezelConfig` / `LlamaCppEngineConfig` satisfy
22
22
  * these shapes by construction.
23
23
  */
24
- import { isGemmaModel } from './kv-cache-type.js';
25
24
  /** Default `--cache-reuse` chunk when neither config nor manifest set it. */
26
25
  export const DEFAULT_CACHE_REUSE = 256;
27
26
  /**
@@ -101,7 +100,7 @@ function normalizeFlagKey(key) {
101
100
  * `args` array.
102
101
  */
103
102
  export function buildLlamaCppEngineArgs(input) {
104
- const { config, perModel, planner, kvCacheType, slots, ggufHasMtp, installedDraftModelPath, reasoningFormat, reasoningPreserve, architecture, modelId, swaFullAutoFits, backend, hostCpuCount, } = input;
103
+ const { config, perModel, planner, kvCacheType, slots, ggufHasMtp, installedDraftModelPath, reasoningFormat, reasoningPreserve, backend, hostCpuCount, } = input;
105
104
  const args = [];
106
105
  // ── GPU-layer offload (`--n-gpu-layers`) ──────────────────────────
107
106
  const nGpuLayers = config.llamaCppNGpuLayers ?? perModel?.nGpuLayers ?? planner?.nGpuLayers;
@@ -176,26 +175,20 @@ export function buildLlamaCppEngineArgs(input) {
176
175
  //
177
176
  // So the engine already declines it precisely when it cannot honour it,
178
177
  // which is what the warning is for. Withholding the flag ourselves only
179
- // cost reuse on multi-slot dense engines. Note the coupling this exposes:
180
- // the memory-driven `--swa-full` decision below also silently decides
181
- // whether Gemma gets any cross-request prefix reuse at all.
178
+ // cost reuse on multi-slot dense engines. Gemma therefore gets it only
179
+ // with a pinned `--swa-full` below.
182
180
  const explicitCacheReuse = config.llamaCppCacheReuse ?? perModel?.cacheReuse;
183
181
  const cacheReuse = explicitCacheReuse ?? DEFAULT_CACHE_REUSE;
184
182
  if (cacheReuse > 0)
185
183
  args.push('--cache-reuse', String(cacheReuse));
186
184
  // ── SWA full cache (`--swa-full`) ─────────────────────────────────
187
- // Tri-state: explicit config/manifest `true`/`false` wins; `undefined`
188
- // at both layers falls through to the Gemma-family auto-default. Gemma's
189
- // sliding-window attention refuses cross-request prefix reuse under the
190
- // memory-efficient windowed cache; the full cache preserves old SWA KV
191
- // for cache operations by allocating full-context storage on every
192
- // cache-owning layer. The cost varies sharply with model geometry, so
193
- // Auto is gated on `swaFullAutoFits`: when weights + full KV do not fit
194
- // fast memory, Gemma launches with the windowed cache instead.
195
- const swaFull = config.llamaCppSwaFull ??
196
- perModel?.swaFull ??
197
- (swaFullAutoFits !== false && isGemmaModel({ architecture, modelId }));
198
- if (swaFull)
185
+ // Opt-in only: an explicit config/manifest `swaFull`. Gemma used to get
186
+ // it by default for cross-request prefix reuse, until a paired A/B on
187
+ // gemma4-31b (2026-10-15) showed the full cache decodes 17-25% slower at
188
+ // every context depth, because decode reads each sliding-window layer's
189
+ // whole cache, and costs 51 GB more at 64K. Trials ran 19% longer in
190
+ // total despite the better reuse.
191
+ if (config.llamaCppSwaFull ?? perModel?.swaFull)
199
192
  args.push('--swa-full');
200
193
  // ── Thread / batch overrides ──────────────────────────────────────
201
194
  // The GPU-resident default needs positive evidence that no model compute
@@ -248,7 +241,8 @@ export function buildLlamaCppEngineArgs(input) {
248
241
  const nMax = config.llamaCppSpecDraftNMax ?? perModel?.spec?.nMax;
249
242
  if (typeof nMax === 'number')
250
243
  args.push('--spec-draft-n-max', String(nMax));
251
- // v0.6.0 supports probabilistic selection only for MTP/simple drafts.
244
+ // v0.7.0 supports probabilistic selection only for MTP/simple drafts
245
+ // (DFlash/LiLiCorr ignore it).
252
246
  // Leave the upstream greedy default untouched unless explicitly set.
253
247
  if ((safeSpecType === 'draft-mtp' || safeSpecType === 'draft-simple') &&
254
248
  config.llamaCppSpecDraftSampling) {
@@ -32,8 +32,9 @@ const GgufValueType = {
32
32
  INT64: 11,
33
33
  FLOAT64: 12,
34
34
  };
35
- // `general.file_type` enum from llama.cpp/src/llama.h. Only the
36
- // common values — the enum has ~30 entries and upstream adds more.
35
+ // `general.file_type` enum (`llama_ftype`) from llama.cpp's include/llama.h.
36
+ // Copy new values from upstream rather than guessing: this table once mapped
37
+ // 30 to BF16 (upstream 30 is IQ4_XS, BF16 is 32) and had no NVFP4 at all.
37
38
  // Anything unrecognised is reported by number.
38
39
  const FILE_TYPE_NAMES = {
39
40
  0: 'ALL_F32',
@@ -54,8 +55,24 @@ const FILE_TYPE_NAMES = {
54
55
  18: 'MOSTLY_Q6_K',
55
56
  19: 'MOSTLY_IQ2_XXS',
56
57
  20: 'MOSTLY_IQ2_XS',
58
+ 21: 'MOSTLY_Q2_K_S',
59
+ 22: 'MOSTLY_IQ3_XS',
57
60
  23: 'MOSTLY_IQ3_XXS',
58
- 30: 'MOSTLY_BF16',
61
+ 24: 'MOSTLY_IQ1_S',
62
+ 25: 'MOSTLY_IQ4_NL',
63
+ 26: 'MOSTLY_IQ3_S',
64
+ 27: 'MOSTLY_IQ3_M',
65
+ 28: 'MOSTLY_IQ2_S',
66
+ 29: 'MOSTLY_IQ2_M',
67
+ 30: 'MOSTLY_IQ4_XS',
68
+ 31: 'MOSTLY_IQ1_M',
69
+ 32: 'MOSTLY_BF16',
70
+ 36: 'MOSTLY_TQ1_0',
71
+ 37: 'MOSTLY_TQ2_0',
72
+ 38: 'MOSTLY_MXFP4_MOE',
73
+ 39: 'MOSTLY_NVFP4',
74
+ 40: 'MOSTLY_Q1_0',
75
+ 41: 'MOSTLY_Q2_0',
59
76
  };
60
77
  /**
61
78
  * The quantization tag a GGUF declares about itself, in the shape the model
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Did a GPU build of llama-server actually get its GPU? Read from the
3
+ * engine's own startup output, because nothing else says.
4
+ *
5
+ * The bundled builds load their GPU backend with dlopen (`GGML_BACKEND_DL`),
6
+ * and in a release build ggml skips a backend that fails to load without a
7
+ * word. A CUDA backend that loads but cannot initialize prints one error line
8
+ * and registers no devices. Either way the engine starts, answers, and runs
9
+ * every token on the CPU — at a fraction of the speed the person's hardware
10
+ * and Settings promise — while gezel reports the CUDA or Vulkan build it
11
+ * launched.
12
+ *
13
+ * llama.cpp names every GPU it will use while loading a model
14
+ * (`using device CUDA0 (NVIDIA GB10) …`), integrated GPUs included. A GPU
15
+ * build that reached readiness after loading a model without that line ran
16
+ * without its GPU. The explicit lines say why when there is a why to say.
17
+ */
18
+ export type GpuUnusedReason =
19
+ /** The CUDA runtime loaded and refused to start: `failed to initialize CUDA`. */
20
+ 'init-failed'
21
+ /** The backend loaded and found no device it could use. */
22
+ | 'no-devices'
23
+ /** No sign of the GPU backend at all: it failed to load, silently. */
24
+ | 'not-loaded';
25
+ export interface GpuUnused {
26
+ backend: 'cuda' | 'vulkan';
27
+ reason: GpuUnusedReason;
28
+ /** The engine's own words, when it gave any. */
29
+ detail?: string;
30
+ }
31
+ export declare class GpuStartupWatch {
32
+ private readonly backend;
33
+ private usingGpu;
34
+ private sawModelLoad;
35
+ private initFailure;
36
+ private noDevices;
37
+ constructor(backend: string | undefined);
38
+ observe(line: string): void;
39
+ /** At readiness: what kept the GPU out, or null when it is in use or this is no GPU build. */
40
+ verdict(): GpuUnused | null;
41
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Did a GPU build of llama-server actually get its GPU? Read from the
3
+ * engine's own startup output, because nothing else says.
4
+ *
5
+ * The bundled builds load their GPU backend with dlopen (`GGML_BACKEND_DL`),
6
+ * and in a release build ggml skips a backend that fails to load without a
7
+ * word. A CUDA backend that loads but cannot initialize prints one error line
8
+ * and registers no devices. Either way the engine starts, answers, and runs
9
+ * every token on the CPU — at a fraction of the speed the person's hardware
10
+ * and Settings promise — while gezel reports the CUDA or Vulkan build it
11
+ * launched.
12
+ *
13
+ * llama.cpp names every GPU it will use while loading a model
14
+ * (`using device CUDA0 (NVIDIA GB10) …`), integrated GPUs included. A GPU
15
+ * build that reached readiness after loading a model without that line ran
16
+ * without its GPU. The explicit lines say why when there is a why to say.
17
+ */
18
+ const USING_GPU = /\busing device (?:CUDA|Vulkan|ROCm|SYCL|MTL)\d+\b/;
19
+ const CUDA_INIT_FAILED = /ggml_cuda_init: failed to initialize CUDA:\s*(.+?)\s*$/;
20
+ const VULKAN_NO_DEVICES = /ggml_vulkan: No devices found/;
21
+ /**
22
+ * Lines llama.cpp prints at the same verbosity as `using device`, while
23
+ * loading a model. Without one the output is too quiet to conclude anything.
24
+ */
25
+ const MODEL_LOAD = /\b(?:llama_model_loader:|load_tensors:|print_info:|llama_model_load_from_file_impl:)/;
26
+ export class GpuStartupWatch {
27
+ backend;
28
+ usingGpu = false;
29
+ sawModelLoad = false;
30
+ initFailure;
31
+ noDevices = false;
32
+ constructor(backend) {
33
+ this.backend = backend;
34
+ }
35
+ observe(line) {
36
+ if (USING_GPU.test(line))
37
+ this.usingGpu = true;
38
+ if (MODEL_LOAD.test(line))
39
+ this.sawModelLoad = true;
40
+ const failure = CUDA_INIT_FAILED.exec(line);
41
+ if (failure?.[1])
42
+ this.initFailure = failure[1];
43
+ if (VULKAN_NO_DEVICES.test(line))
44
+ this.noDevices = true;
45
+ }
46
+ /** At readiness: what kept the GPU out, or null when it is in use or this is no GPU build. */
47
+ verdict() {
48
+ const backend = this.backend;
49
+ if ((backend !== 'cuda' && backend !== 'vulkan') || this.usingGpu)
50
+ return null;
51
+ if (backend === 'cuda' && this.initFailure) {
52
+ return { backend, reason: 'init-failed', detail: this.initFailure };
53
+ }
54
+ if (backend === 'vulkan' && this.noDevices)
55
+ return { backend, reason: 'no-devices' };
56
+ return this.sawModelLoad ? { backend, reason: 'not-loaded' } : null;
57
+ }
58
+ }
@@ -236,6 +236,11 @@ export interface LlamaCppModelManagerOptions {
236
236
  engine: 'llama-cpp' | 'ds4';
237
237
  id: string;
238
238
  }) => void;
239
+ /**
240
+ * The llama-server backend installs are judged against for a catalog
241
+ * source's `excludeBackends`. Test seam — defaults to the process's own.
242
+ */
243
+ resolveBackend?: () => string | undefined;
239
244
  }
240
245
  export declare class LlamaCppModelManager {
241
246
  private readonly modelsRoot;
@@ -244,6 +249,7 @@ export declare class LlamaCppModelManager {
244
249
  private readonly fetchImpl;
245
250
  private readonly engine;
246
251
  private readonly onInstalled?;
252
+ private readonly resolveBackend;
247
253
  /**
248
254
  * In-flight installs. `install()` registers an entry on first yield
249
255
  * and clears it in the iterator's `finally` block, so consumers
@@ -35,13 +35,14 @@ import { createHash } from 'node:crypto';
35
35
  import { createReadStream } from 'node:fs';
36
36
  import { mkdir, readFile, readdir, rename, rm, stat, writeFile } from 'node:fs/promises';
37
37
  import { basename, join, resolve } from 'node:path';
38
- import { createLogger } from '@bendyline/gezel';
38
+ import { createLogger, llamaCppBackendExclusion, llamaCppBackendLabel } from '@bendyline/gezel';
39
39
  import { listBundleModelFiles, publishStagedModel, safeBundleModelPath, } from '../../models/bundle-storage.js';
40
40
  import { evaluateCatalogDrift } from '../../models/catalog-drift.js';
41
41
  import { catalogPayloadFingerprint, describeCatalogPayload, } from '../../models/catalog-payload-identity.js';
42
42
  import { MODEL_HASH_READ_BUFFER_BYTES, assertModelStorePathSafe, findModelRoot, findRenamedModelId, hashModelPayloadFiles, inspectModelDirectory, listIncompleteModelDownloads, listOverlayModelIds, makeSharedModelReadable, modelExistsOnlyReadOnly, modelStorageRoots, pruneModelPayloadFiles, readOnlyModelError, removeModelDir, verifyReadOnlyModelPayload, } from '../../models/storage-roots.js';
43
43
  import { checkDiskSpace, describeDiskShortfall } from '../../utils/disk-space.js';
44
44
  import { downloadWithRetry } from '../../utils/download-with-retry.js';
45
+ import { currentLlamaCppBackend } from './backend-health.js';
45
46
  import { ggufQuantizationTag, readGgufSummary } from './gguf-metadata.js';
46
47
  import { backfillGgufQuantization } from './gguf-quantization-backfill.js';
47
48
  /**
@@ -58,6 +59,7 @@ export class LlamaCppModelManager {
58
59
  fetchImpl;
59
60
  engine;
60
61
  onInstalled;
62
+ resolveBackend;
61
63
  /**
62
64
  * In-flight installs. `install()` registers an entry on first yield
63
65
  * and clears it in the iterator's `finally` block, so consumers
@@ -78,6 +80,7 @@ export class LlamaCppModelManager {
78
80
  this.catalog = opts.catalog;
79
81
  this.fetchImpl = opts.fetchImpl ?? fetch;
80
82
  this.onInstalled = opts.onInstalled;
83
+ this.resolveBackend = opts.resolveBackend ?? currentLlamaCppBackend;
81
84
  }
82
85
  /**
83
86
  * The catalog source block this manager downloads from — `ds4` for the
@@ -435,6 +438,17 @@ export class LlamaCppModelManager {
435
438
  };
436
439
  return;
437
440
  }
441
+ if (this.engine === 'llama-cpp') {
442
+ const backend = this.resolveBackend();
443
+ const excluded = llamaCppBackendExclusion(manifest.llamaCpp, backend);
444
+ if (excluded && backend) {
445
+ yield {
446
+ type: 'error',
447
+ error: `chat-model "${catalogId}" is not available on the ${llamaCppBackendLabel(backend)} llama.cpp engine: ${excluded}`,
448
+ };
449
+ return;
450
+ }
451
+ }
438
452
  tracked.totalBytes = installSrc.approxSizeBytes;
439
453
  yield* this.runInstall(catalogId, manifest, tracked, skipSha, includeMmproj);
440
454
  }
@@ -964,6 +978,10 @@ includeMmproj = true) {
964
978
  // after install without a second download.
965
979
  if (src.draftModel) {
966
980
  out.push({
981
+ ...(src.draftModel.huggingfaceRepo
982
+ ? { huggingfaceRepo: src.draftModel.huggingfaceRepo }
983
+ : {}),
984
+ ...(src.draftModel.revision ? { revision: src.draftModel.revision } : {}),
967
985
  repoPath: src.draftModel.filename,
968
986
  destFilename: basename(src.draftModel.filename),
969
987
  sha256: src.draftModel.sha256,
@@ -286,10 +286,9 @@ export interface WindowedKvLinearization {
286
286
  * than the layer ratio — global layers carry fewer KV heads at wider head
287
287
  * dims (31b: 10 layers × 4 heads × 512+512 vs 50 layers × 16 heads ×
288
288
  * 256+256) — so full-attention math overstates the windowed cache ~14×
289
- * and using it for admission over-clamps or over-denies exactly the
290
- * launches the `--swa-full` decline is trying to keep whole (the
291
- * 2026-08-04 gemma4-26b sweep shipped 19–56K windows on a 65536 request
292
- * this way).
289
+ * and using it for admission over-clamps or over-denies launches the
290
+ * windowed cache holds easily (the 2026-08-04 gemma4-26b sweep shipped
291
+ * 19–56K windows on a 65536 request this way).
293
292
  *
294
293
  * Returns undefined when the header lacks the SWA layout (no window, no
295
294
  * pattern, length mismatches) so callers fall back to their conservative
@@ -323,17 +322,3 @@ export declare function estimateExactPerSlotKvBytesF16(input: Omit<WindowedKvInp
323
322
  */
324
323
  export declare function estimateLinearHybridKvLinearization(input: WindowedKvInput): WindowedKvLinearization | undefined;
325
324
  export declare function estimateWindowedKvLinearization(input: WindowedKvInput): WindowedKvLinearization | undefined;
326
- export interface SwaFullFastMemoryFitInput {
327
- residentWeightsBytes: number;
328
- fullKvBytes: number;
329
- /** Already safety-discounted fast-memory budget (VRAM on a discrete GPU). */
330
- fastBudgetBytes: number;
331
- /** Fast-memory reservations held by models that are already resident. */
332
- committedOtherBytes?: number | undefined;
333
- }
334
- /**
335
- * Whether Auto may keep `--swa-full` without spilling the model or KV cache
336
- * out of the fast pool. The capacity budget already reserves driver/OS
337
- * headroom, so this comparison must not discount it a second time.
338
- */
339
- export declare function fitsSwaFullInFastMemory(input: SwaFullFastMemoryFitInput): boolean;
@@ -406,10 +406,9 @@ const SWA_UBATCH_MARGIN_TOKENS = 2048;
406
406
  * than the layer ratio — global layers carry fewer KV heads at wider head
407
407
  * dims (31b: 10 layers × 4 heads × 512+512 vs 50 layers × 16 heads ×
408
408
  * 256+256) — so full-attention math overstates the windowed cache ~14×
409
- * and using it for admission over-clamps or over-denies exactly the
410
- * launches the `--swa-full` decline is trying to keep whole (the
411
- * 2026-08-04 gemma4-26b sweep shipped 19–56K windows on a 65536 request
412
- * this way).
409
+ * and using it for admission over-clamps or over-denies launches the
410
+ * windowed cache holds easily (the 2026-08-04 gemma4-26b sweep shipped
411
+ * 19–56K windows on a 65536 request this way).
413
412
  *
414
413
  * Returns undefined when the header lacks the SWA layout (no window, no
415
414
  * pattern, length mismatches) so callers fall back to their conservative
@@ -512,13 +511,3 @@ export function estimateWindowedKvLinearization(input) {
512
511
  fixedBytes: Math.round(swaElemsPerToken * bytesPerElement * (slidingWindow + SWA_UBATCH_MARGIN_TOKENS) * loops),
513
512
  };
514
513
  }
515
- /**
516
- * Whether Auto may keep `--swa-full` without spilling the model or KV cache
517
- * out of the fast pool. The capacity budget already reserves driver/OS
518
- * headroom, so this comparison must not discount it a second time.
519
- */
520
- export function fitsSwaFullInFastMemory(input) {
521
- const required = Math.max(0, input.residentWeightsBytes) + Math.max(0, input.fullKvBytes);
522
- const available = Math.max(0, input.fastBudgetBytes - Math.max(0, input.committedOtherBytes ?? 0));
523
- return Number.isFinite(required) && Number.isFinite(available) && required <= available;
524
- }
@@ -22,6 +22,7 @@
22
22
  * lazily respawn its process (see {@link shutdown}).
23
23
  */
24
24
  import { type DisableThinkingRequestShape, LlamaCppSession, type ReasoningEffortRequestShape, type ToolGrammarFallback } from '@bendyline/gezel/local-loop';
25
+ import type { StartupCrashVerdict } from '@bendyline/gezel/native';
25
26
  import type { GpuArbiter } from '../gpu-arbiter.js';
26
27
  import type { NativeEngineLifecycleSnapshot, NativeEngineSupervisor } from '../native/supervisor.js';
27
28
  import { ProviderQueue } from '../queue.js';
@@ -55,6 +56,7 @@ export declare class LlamaCppProvider implements LLMProvider {
55
56
  */
56
57
  private toolGrammarFloor;
57
58
  private readonly supervisor?;
59
+ private readonly startupCrashVerdict?;
58
60
  /** Explicit base URL when no supervisor is managing the process. */
59
61
  private readonly externalBaseUrl?;
60
62
  private readonly defaultModel;
@@ -242,6 +244,12 @@ export declare class LlamaCppProvider implements LLMProvider {
242
244
  * `baseUrl`.
243
245
  */
244
246
  supervisor?: NativeEngineSupervisor;
247
+ /**
248
+ * What a startup crash did to its build's standing (strike or
249
+ * quarantine), looked up by incident id when a start fails, so the error
250
+ * says what the next request will actually do.
251
+ */
252
+ startupCrashVerdict?: (incidentId: string) => StartupCrashVerdict | undefined;
245
253
  /**
246
254
  * Fixed base URL of an already-running llama-server. Used in
247
255
  * dev iteration and LAN-shared setups. Mutually exclusive with
@@ -57,6 +57,7 @@ export class LlamaCppProvider {
57
57
  */
58
58
  toolGrammarFloor = null;
59
59
  supervisor;
60
+ startupCrashVerdict;
60
61
  /** Explicit base URL when no supervisor is managing the process. */
61
62
  externalBaseUrl;
62
63
  defaultModel;
@@ -247,6 +248,8 @@ export class LlamaCppProvider {
247
248
  }
248
249
  if (opts.supervisor)
249
250
  this.supervisor = opts.supervisor;
251
+ if (opts.startupCrashVerdict)
252
+ this.startupCrashVerdict = opts.startupCrashVerdict;
250
253
  if (opts.baseUrl)
251
254
  this.externalBaseUrl = opts.baseUrl.replace(/\/+$/, '');
252
255
  // Only register an evictor when we own the lifecycle. External-baseUrl
@@ -848,7 +851,7 @@ export class LlamaCppProvider {
848
851
  catch (err) {
849
852
  const nativeExit = this.supervisor.lastExitSnapshot?.();
850
853
  if (nativeExit && !nativeExit.expected && nativeExit.exitedAt >= ensureStartedAt) {
851
- throw new NativeEngineCrashedError(nativeExit, err);
854
+ throw new NativeEngineCrashedError(nativeExit, err, this.startupCrashVerdict?.(nativeExit.incidentId));
852
855
  }
853
856
  throw err;
854
857
  }
@@ -27,7 +27,7 @@
27
27
  */
28
28
  import { AsyncLocalStorage } from 'node:async_hooks';
29
29
  import { randomUUID } from 'node:crypto';
30
- import { AwakeBudget, autoCompactedNotice, createAwakeTimeout, createLogger, leaksUntaggedReasoning, turnCancelledMessage, } from '@bendyline/gezel';
30
+ import { AwakeBudget, autoCompactedNotice, createAwakeTimeout, createLogger, isProjectStartingCall, leaksUntaggedReasoning, turnCancelledMessage, } from '@bendyline/gezel';
31
31
  import { leadingSystemMessages, standaloneTurnMessages, toolSurfaceSize, } from '@bendyline/gezel/local-loop';
32
32
  import { computeToolBudgetChars } from '@bendyline/gezel/local-loop';
33
33
  import { extractReasoningWithProfile, profileBehaviorConfig, profileHasBehavior, } from '../../model-profile/runtime.js';
@@ -2656,7 +2656,7 @@ class MlxSession extends StreamingSessionBase {
2656
2656
  output =
2657
2657
  "You already posted a question card to the user earlier in this turn. The second `ask_user_question` call was suppressed so the user only sees one card. END YOUR TURN NOW — the user's answer to the first question will arrive as the next user message; this turn produces no further visible content.";
2658
2658
  }
2659
- else if ((call.function.name === 'start_project' || call.function.name === 'start_job') &&
2659
+ else if (isProjectStartingCall(call.function.name, args) &&
2660
2660
  startedProjectOrJobThisTurn) {
2661
2661
  // Success-led phrasing — wild-caught on gemma4-26b/MLX,
2662
2662
  // the previous "DO NOT call..." wording got misread by
@@ -2753,8 +2753,7 @@ class MlxSession extends StreamingSessionBase {
2753
2753
  // Successful start_project / start_job → arm the
2754
2754
  // per-turn macro guard so subsequent variants get
2755
2755
  // intercepted above before reaching the bridge.
2756
- if ((call.function.name === 'start_project' || call.function.name === 'start_job') &&
2757
- !output.startsWith('ERROR:')) {
2756
+ if (isProjectStartingCall(call.function.name, args) && !output.startsWith('ERROR:')) {
2758
2757
  startedProjectOrJobThisTurn = { tool: call.function.name, firstResult: output };
2759
2758
  }
2760
2759
  }
@@ -14,7 +14,6 @@ so concurrent replicas cannot observe a half-built view.
14
14
 
15
15
  from __future__ import annotations
16
16
 
17
- import fcntl
18
17
  import hashlib
19
18
  import json
20
19
  import os
@@ -23,6 +22,13 @@ import uuid
23
22
  from pathlib import Path
24
23
  from typing import Callable, Optional, Tuple
25
24
 
25
+ try:
26
+ import fcntl
27
+ except ImportError:
28
+ # Windows has no fcntl. The MLX engine never runs there, but this module's
29
+ # stdlib suite does (python-suites.test.ts), so it must still import.
30
+ fcntl = None
31
+
26
32
 
27
33
  PLE_MARKER = ".ple.ple_embedding.ngram_embedding.shards."
28
34
  VIEW_MARKER = "GEZEL_PLE_VIEW.json"
@@ -122,7 +128,8 @@ def prepare_external_ple_view(
122
128
  lock_path = target.parent / f".{target.name}.lock"
123
129
  try:
124
130
  with lock_path.open("a+b") as lock:
125
- fcntl.flock(lock.fileno(), fcntl.LOCK_EX)
131
+ if fcntl is not None:
132
+ fcntl.flock(lock.fileno(), fcntl.LOCK_EX)
126
133
  if _valid_view(target, signature):
127
134
  return str(target), "reused", True
128
135
 
@@ -46,6 +46,14 @@ export interface CapacityBrokerOptions {
46
46
  * {@link computeCapacityBudget}. Omit to read the last published probe.
47
47
  */
48
48
  gpuVramBytes?: number | null;
49
+ /**
50
+ * An integrated GPU's firmware carve-out — see
51
+ * {@link CapacityBudgetInput.reservedGpuBytes}. Travels with
52
+ * `gpuVramBytes`/`unifiedMemory` as one description of the host.
53
+ */
54
+ reservedGpuBytes?: number | null;
55
+ /** What an integrated GPU can reach — see {@link CapacityBudgetInput.gpuAddressableBytes}. */
56
+ gpuAddressableBytes?: number | null;
49
57
  /**
50
58
  * May co-resident models spill into system RAM? `null`/omitted follows
51
59
  * {@link autoAllowRamSpillover} for this host. See
@@ -192,6 +200,8 @@ export declare class CapacityBroker {
192
200
  private readonly systemRamBytes;
193
201
  private readonly unifiedMemory;
194
202
  private readonly gpuVramBytes;
203
+ private readonly reservedGpuBytes;
204
+ private readonly gpuAddressableBytes;
195
205
  private explicitAllowRamSpillover;
196
206
  private readonly reservations;
197
207
  constructor(opts?: CapacityBrokerOptions);
@@ -503,11 +513,9 @@ export declare const MIN_VIABLE_LOCAL_CONTEXT_TOKENS = 65536;
503
513
  /** The same minimum on a host that has to trade context for memory. */
504
514
  export declare const MIN_VIABLE_CONSTRAINED_CONTEXT_TOKENS = 32768;
505
515
  /**
506
- * The most KV cache an automatic choice plans for one llama.cpp engine:
507
- * adaptive growth stops here, and the Gemma `--swa-full` auto-default is
508
- * declined when its full-size cache would pass it. Explicit settings (a
509
- * numeric context, a pinned `swaFull`) are the operator's call and bypass it;
510
- * it never shortens the ladder-settled base window either.
516
+ * The most KV cache adaptive growth plans for one llama.cpp engine. An
517
+ * explicit numeric context is the operator's call and skips growth
518
+ * altogether, and the ceiling never shortens the ladder-settled base window.
511
519
  *
512
520
  * Past this a bigger cache buys nothing: llama.cpp's prefill and decode cost
513
521
  * follows the tokens actually present, not the reservation, so the rest only
@@ -650,10 +658,10 @@ export interface CtxMemoryClampInput {
650
658
  */
651
659
  freeSystemRamBytes?: number;
652
660
  /**
653
- * Usable VRAM on a discrete card; MUST be 0 on unified/shared-pool
654
- * hosts, where the "VRAM" is the same physical RAM `freeSystemRamBytes`
655
- * measures and adding it would double-count. `computeCapacityBudget`
656
- * already reports `vramBytes: 0` for those hosts — pass its value.
661
+ * Usable VRAM on a discrete card. On a unified/shared-pool host it MUST be
662
+ * only an integrated GPU's firmware carve-out, which `freeSystemRamBytes`
663
+ * never sees; any "VRAM" that is the same physical RAM would double-count.
664
+ * `computeCapacityBudget` reports exactly that — pass its value.
657
665
  */
658
666
  vramBytes: number;
659
667
  minPerTurnCtxTokens?: number;
@@ -751,7 +759,7 @@ export interface AdaptiveCtxGrowthInput {
751
759
  fastBudgetBytes: number;
752
760
  committedOtherBytes?: number;
753
761
  budgetKind: CapacityBudget['kind'];
754
- /** Usable VRAM on a discrete card (`liveBudget.vramBytes`); 0 elsewhere. */
762
+ /** Usable VRAM on a discrete card (`liveBudget.vramBytes`); ignored elsewhere. */
755
763
  vramBytes: number;
756
764
  /**
757
765
  * Graphics memory free right now on a discrete card, as device admission
@@ -796,11 +804,9 @@ export interface AdaptiveCtxGrowthResult {
796
804
  * settled slots and granted the un-grown request.
797
805
  *
798
806
  * Deliberately a second clamp pass rather than a bigger first request:
799
- * the first ladder pass sizes slot ceilings, the `--swa-full` fit verdict,
800
- * and RAM-spillover admission, all of which must stay anchored on the
801
- * practical 64K target — growing the request there would decline
802
- * `--swa-full` for models whose full cache fits at 64K and grow KV into
803
- * system-RAM spill on discrete cards.
807
+ * the first ladder pass sizes slot ceilings and RAM-spillover admission,
808
+ * both of which must stay anchored on the practical 64K target — growing
809
+ * the request there would grow KV into system-RAM spill on discrete cards.
804
810
  *
805
811
  * Slot trade: when `allowSlotTrade` is set and the target is out of reach
806
812
  * at the settled slot count, growth walks the count down — taking the