@bendyline/gezel-inference 1.0.7 → 1.0.9

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 (78) hide show
  1. package/dist/catalog/chat-models/ge/gemma4-12b-q4/manifest.json +46 -3
  2. package/dist/catalog/chat-models/ge/gemma4-12b-q4/versions/1.2.1/manifest.json +141 -0
  3. package/dist/catalog/chat-models/ge/gemma4-e4b-q4/manifest.json +5 -0
  4. package/dist/catalog/chat-models/index.json +1 -1
  5. package/dist/catalog/chat-models/or/ornith1.5-9b-q4/manifest.json +5 -0
  6. package/dist/catalog/chat-models/or/ornith1.5-9b-q8/manifest.json +5 -0
  7. package/dist/catalog/chat-models/qw/qwen3.5-9b-q4/manifest.json +5 -0
  8. package/dist/catalog/chat-models/qw/qwen3.8-27b-q4/manifest.json +5 -0
  9. package/dist/catalog/chat-models/qw/qwen3.8-flash-next-q2/manifest.json +24 -7
  10. package/dist/catalog/chat-models/qw/qwen3.8-flash-next-q2/versions/1.0.3/manifest.json +47 -0
  11. package/dist/catalog/chat-models/raw-index.json +1 -1
  12. package/dist/catalog/provenance.json +1 -1
  13. package/dist/engines/electron-native-reuse.d.ts +5 -4
  14. package/dist/engines/electron-native-reuse.js +1 -1
  15. package/dist/engines/native-file-manifest.json +12344 -698
  16. package/dist/engines/native-manifest.d.ts +4 -4
  17. package/dist/engines/native-manifest.js +40 -36
  18. package/dist/engines/resolver.d.ts +18 -17
  19. package/dist/engines/resolver.js +36 -1
  20. package/dist/http/model-inventory-cache.d.ts +1 -1
  21. package/dist/http/routes/engines.js +3 -3
  22. package/dist/http/routes/v1-images.d.ts +2 -1
  23. package/dist/http/routes/v1-images.js +36 -9
  24. package/dist/knowledge/manager.d.ts +3 -27
  25. package/dist/knowledge/manager.js +8 -364
  26. package/dist/knowledge/unified-search.d.ts +61 -0
  27. package/dist/knowledge/unified-search.js +373 -0
  28. package/dist/models/catalog-drift.d.ts +2 -0
  29. package/dist/models/catalog-drift.js +1 -1
  30. package/dist/models/catalog-payload-identity.d.ts +2 -2
  31. package/dist/models/catalog-payload-identity.js +10 -5
  32. package/dist/providers/active-install-message.js +1 -1
  33. package/dist/providers/catalog-model-config.d.ts +28 -0
  34. package/dist/providers/ds4/build-provider.js +35 -13
  35. package/dist/providers/ds4/residency.d.ts +77 -0
  36. package/dist/providers/ds4/residency.js +87 -6
  37. package/dist/providers/image/sd-cpp.js +19 -11
  38. package/dist/providers/llama-cpp/backend-health.d.ts +3 -2
  39. package/dist/providers/llama-cpp/backend-health.js +6 -3
  40. package/dist/providers/llama-cpp/backend-quarantine.js +1 -1
  41. package/dist/providers/llama-cpp/build-provider.js +168 -29
  42. package/dist/providers/llama-cpp/devices.js +4 -1
  43. package/dist/providers/llama-cpp/engine-flags.d.ts +8 -1
  44. package/dist/providers/llama-cpp/engine-flags.js +13 -8
  45. package/dist/providers/llama-cpp/gpu-startup-watch.d.ts +4 -4
  46. package/dist/providers/llama-cpp/gpu-startup-watch.js +7 -5
  47. package/dist/providers/llama-cpp/models.d.ts +10 -0
  48. package/dist/providers/llama-cpp/models.js +64 -10
  49. package/dist/providers/llama-cpp/offload-planner.d.ts +6 -0
  50. package/dist/providers/llama-cpp/offload-planner.js +1 -1
  51. package/dist/providers/llama-cpp/spec-draft.d.ts +45 -23
  52. package/dist/providers/llama-cpp/spec-draft.js +113 -24
  53. package/dist/providers/mlx/async-file-handoff.d.ts +9 -1
  54. package/dist/providers/mlx/async-file-handoff.js +32 -4
  55. package/dist/providers/mlx/provider.js +28 -16
  56. package/dist/providers/mlx/python/spec_decode.py +23 -2
  57. package/dist/providers/mlx/python/tool_args_json.py +59 -21
  58. package/dist/providers/mlx/runtime-diagnostics.d.ts +6 -0
  59. package/dist/providers/mlx/runtime-diagnostics.js +18 -1
  60. package/dist/providers/mlx/tool-call-protocol.d.ts +4 -3
  61. package/dist/providers/mlx/tool-call-protocol.js +4 -3
  62. package/dist/providers/mlx/venv.d.ts +19 -1
  63. package/dist/providers/mlx/venv.js +21 -2
  64. package/dist/providers/native/device-capacity-ledger.d.ts +4 -2
  65. package/dist/providers/native/device-capacity-ledger.js +35 -20
  66. package/dist/providers/native/device-capacity.d.ts +4 -0
  67. package/dist/providers/native/device-capacity.js +5 -0
  68. package/dist/providers/native/local-engine-runtime.d.ts +16 -1
  69. package/dist/providers/native/local-engine-runtime.js +142 -39
  70. package/dist/providers/native/process-identity.d.ts +38 -0
  71. package/dist/providers/native/process-identity.js +86 -0
  72. package/dist/providers/native/supervisor.d.ts +8 -1
  73. package/dist/providers/native/supervisor.js +16 -7
  74. package/dist/providers/types.d.ts +4 -0
  75. package/dist/remotes/types.d.ts +1 -1
  76. package/dist/runtime-lock.d.ts +3 -27
  77. package/dist/runtime-lock.js +7 -80
  78. package/package.json +4 -4
@@ -1,15 +1,17 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { join } from 'node:path';
3
- import { createLogger } from '@bendyline/gezel';
4
- import { estimateLlamaCppResidentBytes } from '@bendyline/gezel';
3
+ import { createLogger, ds4SsdStreamingSupported } from '@bendyline/gezel';
4
+ import { estimateLlamaCppResidentBytes, llamaCppDraftResidentBytes, } from '@bendyline/gezel';
5
5
  import { resolveCatalogLlamaCppEngineConfig } from '../catalog-model-config.js';
6
- import { buildDs4Provider, resolveDs4LaunchCtx, resolveDs4VisionLaunch, } from '../ds4/build-provider.js';
6
+ import { buildDs4Provider, ds4RamTieredContext, resolveDs4LaunchCtx, resolveDs4VisionLaunch, } from '../ds4/build-provider.js';
7
7
  import { ds4VisionResidentBytes } from '../ds4/residency.js';
8
+ import { currentLlamaCppBackend } from '../llama-cpp/backend-health.js';
8
9
  import { buildLlamaCppProvider, ensureLlamaEngineStatus } from '../llama-cpp/build-provider.js';
9
10
  import { readGgufSummaryAsync } from '../llama-cpp/gguf-metadata-async.js';
10
11
  import { effectiveLazyTensorBytes } from '../llama-cpp/gguf-metadata.js';
11
12
  import { isGemmaModel, planLlamaCppKv, resolveLlamaCppKvCacheType, } from '../llama-cpp/kv-cache-type.js';
12
13
  import { estimateExactPerSlotKvBytesF16, estimateKvReserveBytes, estimateWindowedKvLinearization, } from '../llama-cpp/offload-planner.js';
14
+ import { drafterCostsContext, llamaCppSidecarLoads, resolveSpecDraft, } from '../llama-cpp/spec-draft.js';
13
15
  import { buildMlxProvider, resolveMlxEffectiveNumCtx } from '../mlx/build-provider.js';
14
16
  import { readMlxModelGeometry } from '../mlx/model-geometry.js';
15
17
  import { ModelNotInstalledError } from '../types.js';
@@ -594,7 +596,8 @@ export class LocalEngineRuntime {
594
596
  const config = await this.store.readConfig();
595
597
  const externalBaseUrl = process.env.GEZEL_DS4_SERVER_URL ?? config.ds4BaseUrl;
596
598
  if (!externalBaseUrl) {
597
- const { ds4ProjectedResidentBytes, ds4ResidentBytesForMode, ds4ResidentLine, shouldUseDs4SsdStreaming, } = await import('../ds4/residency.js');
599
+ const { ds4ProjectedResidentBytes, ds4ResidentBytesForMode, ds4ResidentLine, planDs4MeasuredLaunch, shouldUseDs4SsdStreaming, } = await import('../ds4/residency.js');
600
+ const { totalMemoryBytes } = await import('../../system/host-memory.js');
598
601
  // Bill the window this model will actually launch with. The
599
602
  // authored footprint is a measurement at `residentCtxTokens`, so
600
603
  // without this a raised context reserves the old number and the
@@ -603,29 +606,51 @@ export class LocalEngineRuntime {
603
606
  const ds4Block = cm.ds4;
604
607
  const installed = await this.ds4Models?.resolveModel(modelId).catch(() => null);
605
608
  const visionResidentBytes = await this.resolveDs4VisionResidentBytes(config, modelId, installed, ds4Block);
606
- const line = ds4ResidentLine({
607
- residentBytes: bytes,
608
- kvBytesPerToken: ds4Block?.kvBytesPerToken,
609
- residentCtxTokens: ds4Block?.residentCtxTokens,
609
+ const totalRamBytes = totalMemoryBytes();
610
+ const ds4Override = config.modelContextOverrides?.[`ds4:${modelId}`];
611
+ const contextFloor = minViableLocalContextTokens();
612
+ const numCtx = resolveDs4LaunchCtx({
613
+ configured: ds4Override ?? config.ds4NumCtx,
614
+ ramTieredCtx: ds4RamTieredContext(totalRamBytes),
615
+ catalogMaxCtx: ds4Block?.maxLaunchCtx,
616
+ minViableContextTokens: ds4Override !== undefined ? Math.min(contextFloor, ds4Override) : contextFloor,
610
617
  });
611
- if (line) {
612
- const { totalMemoryBytes } = await import('../../system/host-memory.js');
613
- bytes = ds4ProjectedResidentBytes(line, resolveDs4LaunchCtx({
614
- configured: config.modelContextOverrides?.[`ds4:${modelId}`] ?? config.ds4NumCtx,
615
- ramTieredCtx: totalMemoryBytes() / 1024 ** 3 >= 192 ? 262_144 : 131_072,
616
- catalogMaxCtx: ds4Block?.maxLaunchCtx,
617
- minViableContextTokens: minViableLocalContextTokens(),
618
- }));
619
- }
620
- bytes = ds4ResidentBytesForMode(bytes, shouldUseDs4SsdStreaming({
618
+ const streamingSupported = ds4SsdStreamingSupported(ds4Block, process.platform);
619
+ const ssdStreaming = shouldUseDs4SsdStreaming({
621
620
  configured: config.ds4SsdStreaming,
622
- ...(ds4Block?.ssdStreamingSupported !== undefined
623
- ? { ssdStreamingSupported: ds4Block.ssdStreamingSupported }
621
+ ...(streamingSupported !== undefined
622
+ ? { ssdStreamingSupported: streamingSupported }
624
623
  : {}),
625
624
  modelSizeBytes: ds4Block?.residentWeightBytes ?? block.approxSizeBytes,
625
+ totalRamBytes,
626
626
  ...(visionResidentBytes > 0 ? { companionBytes: visionResidentBytes } : {}),
627
- }), ds4Block?.ssdStreamingSupported);
628
- bytes += visionResidentBytes;
627
+ });
628
+ const measured = planDs4MeasuredLaunch({
629
+ source: ds4Block,
630
+ numCtx,
631
+ ssdStreaming,
632
+ totalRamBytes,
633
+ budgetBytes: ssdStreaming && ds4Block?.streamingMemory
634
+ ? await (await import('./device-capacity.js')).deviceModelBudgetBytes(config)
635
+ : undefined,
636
+ sidecarBytes: visionResidentBytes,
637
+ configuredCacheGb: config.ds4CacheExpertsGb,
638
+ });
639
+ if (measured) {
640
+ bytes = measured.workingSetBytes;
641
+ }
642
+ else {
643
+ const line = ds4ResidentLine({
644
+ residentBytes: bytes,
645
+ kvBytesPerToken: ds4Block?.kvBytesPerToken,
646
+ residentCtxTokens: ds4Block?.residentCtxTokens,
647
+ });
648
+ if (line)
649
+ bytes = ds4ProjectedResidentBytes(line, numCtx);
650
+ bytes =
651
+ ds4ResidentBytesForMode(bytes, ssdStreaming, streamingSupported) +
652
+ visionResidentBytes;
653
+ }
629
654
  }
630
655
  }
631
656
  }
@@ -1047,7 +1072,9 @@ export class LocalEngineRuntime {
1047
1072
  throw new ModelNotInstalledError(name, modelId);
1048
1073
  }
1049
1074
  const { totalMemoryBytes } = await import('../../system/host-memory.js');
1050
- const ramTieredCtx = totalMemoryBytes() / 1024 ** 3 >= 192 ? 262_144 : 131_072;
1075
+ // The launcher's own tier: 64K below 96 GiB, so a smaller Mac is quoted
1076
+ // the window and footprint it will actually launch with.
1077
+ const ramTieredCtx = ds4RamTieredContext(totalMemoryBytes());
1051
1078
  const ds4Override = config.modelContextOverrides?.[`ds4:${modelId}`];
1052
1079
  const ds4Floor = ds4Override !== undefined ? Math.min(contextFloor, ds4Override) : contextFloor;
1053
1080
  const effective = resolveDs4LaunchCtx({
@@ -1086,36 +1113,69 @@ export class LocalEngineRuntime {
1086
1113
  // `fixed + slope × ctx` line llama.cpp/MLX rows carry, so the slider
1087
1114
  // prices a drag instead of claiming the footprint never moves. Entries
1088
1115
  // without a measured slope keep the flat number and say so.
1089
- const { ds4ProjectedResidentBytes, ds4ResidentBytesForMode, ds4ResidentLine, shouldUseDs4SsdStreaming, } = await import('../ds4/residency.js');
1116
+ const { ds4ProjectedResidentBytes, ds4ResidentBytesForMode, ds4ResidentLine, planDs4MeasuredLaunch, shouldUseDs4SsdStreaming, } = await import('../ds4/residency.js');
1090
1117
  const ds4Line = ds4ResidentLine({
1091
1118
  residentBytes: ds4Source?.residentBytes,
1092
1119
  kvBytesPerToken: ds4Source?.kvBytesPerToken,
1093
1120
  residentCtxTokens: ds4Source?.residentCtxTokens,
1094
1121
  });
1095
1122
  const visionResidentBytes = await this.resolveDs4VisionResidentBytes(config, modelId, installed, ds4Source);
1123
+ const streamingSupported = ds4SsdStreamingSupported(ds4Source, process.platform);
1096
1124
  const ssdStreaming = shouldUseDs4SsdStreaming({
1097
1125
  configured: config.ds4SsdStreaming,
1098
- ...(ds4Source?.ssdStreamingSupported !== undefined
1099
- ? { ssdStreamingSupported: ds4Source.ssdStreamingSupported }
1100
- : {}),
1126
+ ...(streamingSupported !== undefined ? { ssdStreamingSupported: streamingSupported } : {}),
1101
1127
  modelSizeBytes: ds4Source?.residentWeightBytes ??
1102
1128
  ds4Source?.approxSizeBytes ??
1103
1129
  installed?.approxSizeBytes,
1104
1130
  ...(visionResidentBytes > 0 ? { companionBytes: visionResidentBytes } : {}),
1105
1131
  });
1106
- const plannedResidentBytes = ds4Line
1107
- ? ds4ResidentBytesForMode(ds4ProjectedResidentBytes(ds4Line, effective), ssdStreaming, ds4Source?.ssdStreamingSupported) + visionResidentBytes
1108
- : await this.resolveResidentBytes('ds4', modelId);
1132
+ // A measurement for this launch's mode prices it exactly — the window
1133
+ // moves only that mode's slope, and a streamed launch carries the cache
1134
+ // the launcher would choose. Unmeasured entries keep the authored line.
1135
+ const measured = planDs4MeasuredLaunch({
1136
+ source: ds4Source,
1137
+ numCtx: effective,
1138
+ ssdStreaming,
1139
+ totalRamBytes: totalMemoryBytes(),
1140
+ budgetBytes: ssdStreaming && ds4Source?.streamingMemory
1141
+ ? await (await import('./device-capacity.js')).deviceModelBudgetBytes(config)
1142
+ : undefined,
1143
+ sidecarBytes: visionResidentBytes,
1144
+ configuredCacheGb: config.ds4CacheExpertsGb,
1145
+ });
1146
+ const plannedResidentBytes = measured
1147
+ ? measured.workingSetBytes
1148
+ : ds4Line
1149
+ ? ds4ResidentBytesForMode(ds4ProjectedResidentBytes(ds4Line, effective), ssdStreaming, streamingSupported) + visionResidentBytes
1150
+ : await this.resolveResidentBytes('ds4', modelId);
1109
1151
  return {
1110
1152
  contextWindow: useResidentOr(effective, Math.min(effective, ds4Floor)),
1111
1153
  plannedResidentBytes,
1112
- ...(ds4Line
1154
+ ssdStreaming,
1155
+ ...(measured
1113
1156
  ? {
1114
- weightsResidentBytes: ds4ResidentBytesForMode(ds4Line.contextFreeBytes, ssdStreaming, ds4Source?.ssdStreamingSupported) + visionResidentBytes,
1157
+ memoryMeasured: true,
1158
+ weightsResidentBytes: measured.contextFreeBytes,
1115
1159
  kvFixedBytesPerSlot: 0,
1116
- kvBytesPerTokenPerSlot: ds4Line.kvBytesPerToken,
1160
+ kvBytesPerTokenPerSlot: measured.bytesPerToken,
1161
+ ...(measured.cache
1162
+ ? {
1163
+ expertCache: {
1164
+ cacheBytes: measured.cache.cacheGb * 1024 ** 3,
1165
+ requestedBytes: measured.cache.requestedGb * 1024 ** 3,
1166
+ clamped: measured.cache.clamped,
1167
+ safe: measured.cache.safe,
1168
+ },
1169
+ }
1170
+ : {}),
1117
1171
  }
1118
- : {}),
1172
+ : ds4Line
1173
+ ? {
1174
+ weightsResidentBytes: ds4ResidentBytesForMode(ds4Line.contextFreeBytes, ssdStreaming, streamingSupported) + visionResidentBytes,
1175
+ kvFixedBytesPerSlot: 0,
1176
+ kvBytesPerTokenPerSlot: ds4Line.kvBytesPerToken,
1177
+ }
1178
+ : {}),
1119
1179
  ...(nativeWindow ? { nativeContextWindow: nativeWindow } : {}),
1120
1180
  ...(ds4Override !== undefined ? { overrideContextTokens: ds4Override } : {}),
1121
1181
  ...(ds4AutoContextWindow !== undefined ? { autoContextWindow: ds4AutoContextWindow } : {}),
@@ -1190,9 +1250,32 @@ export class LocalEngineRuntime {
1190
1250
  ? effectiveLazyTensorBytes(summary, config.llamaCppLazyMode ?? manifestEngineConfig?.lazyMode ?? 'auto')
1191
1251
  : 0;
1192
1252
  const residentWeightFileBytes = Math.max(0, installed.approxSizeBytes - lazyTensorBytes);
1193
- const residentWeightsBytes = estimateLlamaCppResidentBytes(residentWeightFileBytes, {
1253
+ const { perModel: previewEngineConfig } = await resolveSpecDraft({
1254
+ perModel: manifestEngineConfig,
1255
+ backend: currentLlamaCppBackend(),
1256
+ configSpecType: config.llamaCppSpecType,
1257
+ configDraftModelPath: config.llamaCppDraftModelPath,
1258
+ installedDraftModelPath: installed.draftModelPath,
1259
+ resolveDraftPath: async (id) => (await this.llamaCppModels?.resolveModelPath(id)) ?? null,
1260
+ });
1261
+ const previewSpecType = config.llamaCppSpecType ?? previewEngineConfig?.spec?.type;
1262
+ const draftBudgetBytes = llamaCppSidecarLoads({
1263
+ specType: previewSpecType,
1264
+ targetHasMtp: (summary?.nextnPredictLayers ?? 0) > 0,
1265
+ perBackend: !!manifestEngineConfig?.spec?.byBackend,
1266
+ }) && installed.draftModelPath
1267
+ ? (installed.draftModelSizeBytes ?? 0)
1268
+ : 0;
1269
+ const catalogDetail = draftBudgetBytes > 0 ? await this.catalog.get('chat-model', modelId).catch(() => null) : null;
1270
+ const draftResidentBytes = llamaCppDraftResidentBytes(catalogDetail?.manifest.kind === 'chat-model'
1271
+ ? catalogDetail.manifest.llamaCpp?.draftModel
1272
+ : undefined, installed.draftModelPath);
1273
+ const residentBytesWith = (draftBytes) => estimateLlamaCppResidentBytes(residentWeightFileBytes, {
1194
1274
  mmprojBytes: visionBudgetBytes,
1275
+ draftBytes,
1276
+ draftResidentBytes,
1195
1277
  });
1278
+ let residentWeightsBytes = residentBytesWith(draftBudgetBytes);
1196
1279
  const exactPerSlotKvF16 = summary
1197
1280
  ? estimateExactPerSlotKvBytesF16({
1198
1281
  blockCount: summary.blockCount,
@@ -1452,6 +1535,7 @@ export class LocalEngineRuntime {
1452
1535
  maxSlots: defaultLocalEngineSlots(fastBudgetBytes),
1453
1536
  });
1454
1537
  kv = kvPlan.kvCacheType;
1538
+ const draftFits = planFitsAt(kv, kvPlan.ctxCapTokens ?? grantedCtx, 1);
1455
1539
  // Mirror the launch: the f16-by-context-cap trade shrinks the granted
1456
1540
  // window here too, so the settings preview shows what will really run.
1457
1541
  if (kvPlan.ctxCapTokens !== undefined && kvPlan.ctxCapTokens < grantedCtx) {
@@ -1462,7 +1546,7 @@ export class LocalEngineRuntime {
1462
1546
  ceiling: passCeiling(kv, grantedCtx),
1463
1547
  tierDefault: defaultLocalEngineSlots(fastBudgetBytes),
1464
1548
  });
1465
- if ((config.llamaCppSpecType ?? manifestEngineConfig?.spec?.type) === 'draft-mtp')
1549
+ if (previewSpecType === 'draft-mtp')
1466
1550
  slots = 1;
1467
1551
  try {
1468
1552
  if (!summary)
@@ -1544,7 +1628,14 @@ export class LocalEngineRuntime {
1544
1628
  // SWA model without a readable layout, over budget on full
1545
1629
  // math: the launch path leaves such a launch untouched — preview
1546
1630
  // the requested window.
1547
- return { grantedCtx, slots, kvCacheType: kv, plannedFieldsOk: false };
1631
+ return {
1632
+ grantedCtx,
1633
+ slots,
1634
+ kvCacheType: kv,
1635
+ plannedFieldsOk: false,
1636
+ draftFits,
1637
+ kvPlan,
1638
+ };
1548
1639
  }
1549
1640
  }
1550
1641
  if (!admission.minimumSatisfied) {
@@ -1588,9 +1679,21 @@ export class LocalEngineRuntime {
1588
1679
  throw error;
1589
1680
  log.warn(`[llama-cpp] could not inspect ${modelId} while previewing admission: ${error instanceof Error ? error.message : String(error)}`);
1590
1681
  }
1591
- return { grantedCtx, slots, kvCacheType: kv, plannedFieldsOk: true };
1682
+ return { grantedCtx, slots, kvCacheType: kv, plannedFieldsOk: true, draftFits, kvPlan };
1592
1683
  };
1593
- const plan = llamaPlanPass(explicitContextWindow, contextFloor);
1684
+ let plan = llamaPlanPass(explicitContextWindow, contextFloor);
1685
+ // The launch leaves out a catalog drafter that would cap the target window
1686
+ // or cost its KV precision; the preview has to land on the same plan.
1687
+ if (draftBudgetBytes > 0 && !config.llamaCppSpecType && !config.llamaCppDraftModelPath) {
1688
+ residentWeightsBytes = residentBytesWith(0);
1689
+ const withoutDrafter = llamaPlanPass(explicitContextWindow, contextFloor);
1690
+ if (drafterCostsContext(plan.kvPlan, withoutDrafter.kvPlan, plan.draftFits)) {
1691
+ plan = withoutDrafter;
1692
+ }
1693
+ else {
1694
+ residentWeightsBytes = residentBytesWith(draftBudgetBytes);
1695
+ }
1696
+ }
1594
1697
  let autoContextWindow;
1595
1698
  if (overrideActive) {
1596
1699
  try {
@@ -1,2 +1,40 @@
1
+ export declare function processAlive(pid: number): boolean;
1
2
  /** Birth stamps survive coordinator restarts and distinguish a reused PID. */
2
3
  export declare function readProcessIdentities(pids: readonly number[]): Promise<ReadonlyMap<number, string>>;
4
+ /**
5
+ * How long a stamp read for a still-live PID is trusted. A PID can die and be
6
+ * reused between two liveness checks; this bounds how long such a dead
7
+ * engine's reservation outlives it. That is the safe direction: memory looks
8
+ * taken for a few seconds longer, never free twice.
9
+ */
10
+ export declare const VERIFIED_IDENTITY_TTL_MS = 10000;
11
+ export interface ProcessIdentityCacheOptions {
12
+ read?: (pids: readonly number[]) => Promise<ReadonlyMap<number, string>>;
13
+ alive?: (pid: number) => boolean;
14
+ now?: () => number;
15
+ ttlMs?: number;
16
+ }
17
+ /**
18
+ * Reading a birth stamp starts `ps`, or PowerShell on Windows. The capacity
19
+ * ledger used to read every PID it holds on each command, which ran before
20
+ * every engine request. A stamp read for a PID that is still alive is reused
21
+ * for a short while, and the PIDs that do need reading go in one batch.
22
+ *
23
+ * A cached stamp may only confirm the stamp a reservation already recorded.
24
+ * A stamp about to be recorded, or one that disagrees, is read again: another
25
+ * coordinator may have recorded a newer process at that PID, and trusting an
26
+ * older stamp there would free a live engine's memory.
27
+ */
28
+ export declare class ProcessIdentityCache {
29
+ private readonly options;
30
+ private readonly verified;
31
+ constructor(options?: ProcessIdentityCacheOptions);
32
+ /**
33
+ * `record`: PIDs whose stamp is about to be written onto a reservation.
34
+ * `confirm`: each PID's stamps as reservations recorded them.
35
+ * PIDs that are dead, or whose stamp cannot be read, are left out.
36
+ */
37
+ lookup(record: Iterable<number>, confirm: ReadonlyMap<number, readonly string[]>): Promise<ReadonlyMap<number, string>>;
38
+ }
39
+ /** One cache per process: every ledger in it asks about the same OS processes. */
40
+ export declare const sharedProcessIdentities: ProcessIdentityCache;
@@ -11,6 +11,15 @@ const PROBE_PID_LIMIT = {
11
11
  darwin: 99_999,
12
12
  win32: 2_147_483_647,
13
13
  };
14
+ export function processAlive(pid) {
15
+ try {
16
+ process.kill(pid, 0);
17
+ return true;
18
+ }
19
+ catch (error) {
20
+ return error.code !== 'ESRCH';
21
+ }
22
+ }
14
23
  /** Birth stamps survive coordinator restarts and distinguish a reused PID. */
15
24
  export async function readProcessIdentities(pids) {
16
25
  const limit = PROBE_PID_LIMIT[process.platform] ?? Number.MAX_SAFE_INTEGER;
@@ -68,3 +77,80 @@ export async function readProcessIdentities(pids) {
68
77
  }
69
78
  return identities;
70
79
  }
80
+ /**
81
+ * How long a stamp read for a still-live PID is trusted. A PID can die and be
82
+ * reused between two liveness checks; this bounds how long such a dead
83
+ * engine's reservation outlives it. That is the safe direction: memory looks
84
+ * taken for a few seconds longer, never free twice.
85
+ */
86
+ export const VERIFIED_IDENTITY_TTL_MS = 10_000;
87
+ /**
88
+ * Reading a birth stamp starts `ps`, or PowerShell on Windows. The capacity
89
+ * ledger used to read every PID it holds on each command, which ran before
90
+ * every engine request. A stamp read for a PID that is still alive is reused
91
+ * for a short while, and the PIDs that do need reading go in one batch.
92
+ *
93
+ * A cached stamp may only confirm the stamp a reservation already recorded.
94
+ * A stamp about to be recorded, or one that disagrees, is read again: another
95
+ * coordinator may have recorded a newer process at that PID, and trusting an
96
+ * older stamp there would free a live engine's memory.
97
+ */
98
+ export class ProcessIdentityCache {
99
+ options;
100
+ verified = new Map();
101
+ constructor(options = {}) {
102
+ this.options = options;
103
+ }
104
+ /**
105
+ * `record`: PIDs whose stamp is about to be written onto a reservation.
106
+ * `confirm`: each PID's stamps as reservations recorded them.
107
+ * PIDs that are dead, or whose stamp cannot be read, are left out.
108
+ */
109
+ async lookup(record, confirm) {
110
+ const alive = this.options.alive ?? processAlive;
111
+ const ttl = this.options.ttlMs ?? VERIFIED_IDENTITY_TTL_MS;
112
+ const now = (this.options.now ?? (() => performance.now()))();
113
+ for (const [pid, entry] of this.verified) {
114
+ if (now - entry.at >= ttl)
115
+ this.verified.delete(pid);
116
+ }
117
+ const live = (pid) => {
118
+ if (alive(pid))
119
+ return true;
120
+ // Forget a PID seen dead, so whatever process takes it next is read.
121
+ this.verified.delete(pid);
122
+ return false;
123
+ };
124
+ const identities = new Map();
125
+ const unread = new Set();
126
+ for (const pid of record)
127
+ if (live(pid))
128
+ unread.add(pid);
129
+ for (const [pid, stamps] of confirm) {
130
+ if (unread.has(pid) || !live(pid))
131
+ continue;
132
+ const cached = this.verified.get(pid)?.identity;
133
+ if (cached !== undefined && stamps.every((stamp) => stamp === cached)) {
134
+ identities.set(pid, cached);
135
+ }
136
+ else {
137
+ unread.add(pid);
138
+ }
139
+ }
140
+ if (unread.size === 0)
141
+ return identities;
142
+ const read = await (this.options.read ?? readProcessIdentities)([...unread]).catch(() => new Map());
143
+ for (const pid of unread) {
144
+ const identity = read.get(pid);
145
+ if (identity === undefined) {
146
+ this.verified.delete(pid);
147
+ continue;
148
+ }
149
+ this.verified.set(pid, { identity, at: now });
150
+ identities.set(pid, identity);
151
+ }
152
+ return identities;
153
+ }
154
+ }
155
+ /** One cache per process: every ledger in it asks about the same OS processes. */
156
+ export const sharedProcessIdentities = new ProcessIdentityCache();
@@ -516,7 +516,14 @@ export declare class NativeEngineSupervisor {
516
516
  private clearFreezeTimer;
517
517
  private clearHealthTimer;
518
518
  }
519
- /** High-precision fatal signatures; attribution happens only if the child exits. */
519
+ /**
520
+ * High-precision fatal signatures; attribution happens only if the child exits.
521
+ *
522
+ * The `cuda-*` kinds also cover ROCm builds: ggml's ROCm backend is its CUDA
523
+ * code compiled with HIP, and it prints the same errors under the name `ROCm`
524
+ * (`ROCm error: out of memory`). The kinds are wire values, so they keep their
525
+ * names rather than growing a ROCm twin each.
526
+ */
520
527
  export declare function classifyNativeEnginePanic(line: string): {
521
528
  kind: NativeEnginePanicKind;
522
529
  line: string;
@@ -1415,13 +1415,20 @@ export class NativeEngineSupervisor {
1415
1415
  }
1416
1416
  }
1417
1417
  }
1418
- /** High-precision fatal signatures; attribution happens only if the child exits. */
1418
+ /**
1419
+ * High-precision fatal signatures; attribution happens only if the child exits.
1420
+ *
1421
+ * The `cuda-*` kinds also cover ROCm builds: ggml's ROCm backend is its CUDA
1422
+ * code compiled with HIP, and it prints the same errors under the name `ROCm`
1423
+ * (`ROCm error: out of memory`). The kinds are wire values, so they keep their
1424
+ * names rather than growing a ROCm twin each.
1425
+ */
1419
1426
  export function classifyNativeEnginePanic(line) {
1420
1427
  const clipped = line.trim().slice(0, 1_000);
1421
- if (/CUDA error:\s*invalid (?:configuration )?argument/i.test(line)) {
1428
+ if (/(?:CUDA|ROCm) error:\s*invalid (?:configuration )?argument/i.test(line)) {
1422
1429
  return { kind: 'cuda-invalid-argument', line: clipped };
1423
1430
  }
1424
- if (/CUDA error:\s*(?:out of memory|memory allocation)/i.test(line)) {
1431
+ if (/(?:CUDA|ROCm) error:\s*(?:out of memory|memory allocation)/i.test(line)) {
1425
1432
  return { kind: 'cuda-out-of-memory', line: clipped };
1426
1433
  }
1427
1434
  // Vulkan's spellings of the same condition — the backend a non-CUDA
@@ -1429,16 +1436,18 @@ export function classifyNativeEnginePanic(line) {
1429
1436
  if (/ErrorOutOfDeviceMemory|OutOfDeviceMemoryError|ggml_vulkan:.*(?:allocation|memory).*failed/i.test(line)) {
1430
1437
  return { kind: 'vulkan-out-of-memory', line: clipped };
1431
1438
  }
1432
- if (/CUDA error:\s*(?:an )?illegal memory access/i.test(line)) {
1439
+ if (/(?:CUDA|ROCm) error:\s*(?:an )?illegal memory access/i.test(line)) {
1433
1440
  return { kind: 'cuda-illegal-memory-access', line: clipped };
1434
1441
  }
1435
- if (/CUDA error:.*device-side assert/i.test(line)) {
1442
+ if (/(?:CUDA|ROCm) error:.*device-side assert/i.test(line)) {
1436
1443
  return { kind: 'cuda-device-assert', line: clipped };
1437
1444
  }
1438
- if (/CUDA error:\s*(?:no kernel image is available|the provided PTX was compiled with an unsupported toolchain)/i.test(line)) {
1445
+ if (/(?:CUDA|ROCm) error:\s*(?:no kernel image is available|invalid device function|the provided PTX was compiled with an unsupported toolchain)/i.test(line) ||
1446
+ // rocBLAS names the architecture it has no kernel library for, then aborts.
1447
+ /rocBLAS error: Cannot read .*TensileLibrary.*for GPU arch/i.test(line)) {
1439
1448
  return { kind: 'cuda-unsupported-device', line: clipped };
1440
1449
  }
1441
- if (/CUDA error:/i.test(line))
1450
+ if (/(?:CUDA|ROCm) error:/i.test(line))
1442
1451
  return { kind: 'cuda-error', line: clipped };
1443
1452
  if (/assert(?:ion)? failed|GGML_ASSERT/i.test(line)) {
1444
1453
  return { kind: 'assertion-failed', line: clipped };
@@ -600,6 +600,10 @@ export interface EngineLaunchSnapshot {
600
600
  export interface ModelInfo {
601
601
  id: string;
602
602
  name: string;
603
+ /** One short line a picker shows beside the name. */
604
+ description?: string;
605
+ /** Heading a picker lists this model under; see core's `ModelInfoSchema`. */
606
+ group?: string;
603
607
  supportsReasoning?: boolean;
604
608
  reasoningEfforts?: string[];
605
609
  defaultReasoningEffort?: string;
@@ -11,7 +11,7 @@ export interface PairedRemote {
11
11
  remoteId: string;
12
12
  /** Base URL A reaches the server at, e.g. `https://192.168.1.50:6229`. */
13
13
  baseUrl: string;
14
- /** Human label shown in A's model picker / Remote Servers panel. */
14
+ /** Human label shown in A's model picker / Remote servers panel. */
15
15
  displayName: string;
16
16
  /** Per-device bearer token the server issued A (scope `remote-inference`). */
17
17
  token: string;
@@ -1,3 +1,6 @@
1
+ import { type LockIdentity, currentLockIdentity, parseLockFile, procStartTicks, sharesPidNamespace } from '@bendyline/gezel/native';
2
+ export type { LockIdentity };
3
+ export { currentLockIdentity, parseLockFile, procStartTicks, sharesPidNamespace };
1
4
  /**
2
5
  * Thrown when a gezel home already has a live daemon. The CLI's `gezel
3
6
  * start` / the supervisor surface this as an actionable message rather
@@ -12,33 +15,6 @@ export declare class SingleInstanceError extends Error {
12
15
  export interface RuntimeLock {
13
16
  release(): Promise<void>;
14
17
  }
15
- /**
16
- * Who holds a lock. A pid alone identifies a process only inside one pid
17
- * namespace on one boot: two containers sharing a volume each see the
18
- * other's pid as dead (or as an unrelated process), which let a one-shot
19
- * `docker run … gezel do` reclaim a serving container's live lock. The
20
- * namespace, boot and host fields say when a pid check is meaningful; a
21
- * holder outside our namespace is judged by the lock file's heartbeat.
22
- */
23
- export interface LockIdentity {
24
- pid: number;
25
- hostname?: string;
26
- bootId?: string;
27
- pidNs?: string;
28
- /** Process start time in clock ticks since boot (`/proc/<pid>/stat` field 22). */
29
- startTicks?: string;
30
- /** Random per-process id; tells our own heartbeat whether the lock is still ours. */
31
- instance?: string;
32
- }
33
- export declare function procStartTicks(pid: number | 'self'): string | undefined;
34
- export declare function currentLockIdentity(): LockIdentity;
35
- export declare function parseLockFile(text: string): LockIdentity | null;
36
- /**
37
- * Whether a pid check against `holder` means anything here. Any field the
38
- * holder recorded must match ours; a legacy pid-only record is assumed to be
39
- * in our namespace, which is how it was judged before.
40
- */
41
- export declare function sharesPidNamespace(holder: LockIdentity, me: LockIdentity): boolean;
42
18
  /**
43
19
  * Acquire an exclusive single-instance lock for a gezel home so two daemons
44
20
  * can't share one `~/.gezel/` and race its on-disk state (config, sessions,