@bendyline/gezel-inference 1.0.0 → 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 (116) hide show
  1. package/README.md +90 -8
  2. package/dist/cache/adapter.d.ts +7 -0
  3. package/dist/cache/controller.d.ts +12 -0
  4. package/dist/cache/controller.js +24 -0
  5. package/dist/catalog/chat-models/bt/btl4-35b-q4/manifest.json +1 -0
  6. package/dist/catalog/chat-models/gl/glm-5.2-754b-q2/manifest.json +1 -0
  7. package/dist/catalog/chat-models/index.json +1 -1
  8. package/dist/catalog/chat-models/lf/lfm2.5-24b-a2b-q4/manifest.json +122 -0
  9. package/dist/catalog/chat-models/lf/lfm2.5-24b-a2b-q4/versions/1.0.0/manifest.json +15 -0
  10. package/dist/catalog/chat-models/mi/minicpm-v4.7-35b-a3b-q4/manifest.json +185 -0
  11. package/dist/catalog/chat-models/mi/minicpm-v4.7-35b-a3b-q4/versions/1.0.0/manifest.json +94 -0
  12. package/dist/catalog/chat-models/mi/minicpm5-1b-q4/manifest.json +1 -0
  13. package/dist/catalog/chat-models/mi/minicpm5-2b-q4/manifest.json +1 -0
  14. package/dist/catalog/chat-models/qw/qwen3.5-0.8b-q4/manifest.json +1 -0
  15. package/dist/catalog/chat-models/raw-index.json +1 -1
  16. package/dist/catalog/provenance.json +1 -1
  17. package/dist/engines/electron-native-reuse.d.ts +9 -0
  18. package/dist/engines/electron-native-reuse.js +146 -45
  19. package/dist/engines/native-file-manifest.json +404 -404
  20. package/dist/engines/native-manifest.d.ts +11 -3
  21. package/dist/engines/native-manifest.js +48 -18
  22. package/dist/http/openai-compat/apple-model.d.ts +2 -0
  23. package/dist/http/openai-compat/apple-model.js +21 -1
  24. package/dist/http/openai-compat/chat-target.d.ts +12 -0
  25. package/dist/http/openai-compat/chat-target.js +27 -1
  26. package/dist/http/openai-compat/engine-failure.d.ts +25 -0
  27. package/dist/http/openai-compat/engine-failure.js +76 -0
  28. package/dist/http/openai-compat/responses-streaming.js +2 -1
  29. package/dist/http/openai-compat/responses-translate.d.ts +1 -1
  30. package/dist/http/openai-compat/translate.d.ts +1 -1
  31. package/dist/http/routes/engines.js +44 -3
  32. package/dist/http/routes/v1-chat.js +30 -8
  33. package/dist/http/routes/v1-models.d.ts +6 -0
  34. package/dist/http/routes/v1-models.js +137 -30
  35. package/dist/http/routes/v1-responses.js +16 -3
  36. package/dist/in-flight-requests.d.ts +19 -0
  37. package/dist/in-flight-requests.js +34 -0
  38. package/dist/inference-service.js +3 -3
  39. package/dist/knowledge/catalog-host.d.ts +9 -1
  40. package/dist/knowledge/catalog-host.js +2 -0
  41. package/dist/knowledge/manager.d.ts +14 -0
  42. package/dist/knowledge/manager.js +53 -6
  43. package/dist/knowledge/search-worker.d.ts +1 -1
  44. package/dist/knowledge/spatial-query.d.ts +2 -3
  45. package/dist/knowledge/spatial-query.js +12 -6
  46. package/dist/knowledge/worker-host.js +2 -0
  47. package/dist/memory/embed-core.d.ts +16 -0
  48. package/dist/memory/embed-core.js +31 -0
  49. package/dist/memory/embed-worker.d.ts +7 -3
  50. package/dist/memory/embed-worker.js +13 -4
  51. package/dist/memory/embeddings.d.ts +17 -2
  52. package/dist/memory/embeddings.js +36 -7
  53. package/dist/models/ensure.d.ts +2 -0
  54. package/dist/models/ensure.js +9 -0
  55. package/dist/perf/responsiveness.d.ts +14 -1
  56. package/dist/perf/responsiveness.js +77 -7
  57. package/dist/providers/llama-cpp/backend-health.d.ts +30 -0
  58. package/dist/providers/llama-cpp/backend-health.js +64 -0
  59. package/dist/providers/llama-cpp/backend-quarantine.d.ts +40 -0
  60. package/dist/providers/llama-cpp/backend-quarantine.js +100 -0
  61. package/dist/providers/llama-cpp/build-provider.js +140 -118
  62. package/dist/providers/llama-cpp/cache-adapter.d.ts +12 -0
  63. package/dist/providers/llama-cpp/cache-adapter.js +36 -1
  64. package/dist/providers/llama-cpp/devices.d.ts +11 -2
  65. package/dist/providers/llama-cpp/devices.js +10 -4
  66. package/dist/providers/llama-cpp/engine-flags.d.ts +0 -18
  67. package/dist/providers/llama-cpp/engine-flags.js +12 -18
  68. package/dist/providers/llama-cpp/gguf-metadata-async.js +7 -0
  69. package/dist/providers/llama-cpp/gguf-metadata.js +20 -3
  70. package/dist/providers/llama-cpp/gpu-startup-watch.d.ts +41 -0
  71. package/dist/providers/llama-cpp/gpu-startup-watch.js +58 -0
  72. package/dist/providers/llama-cpp/models.d.ts +6 -0
  73. package/dist/providers/llama-cpp/models.js +19 -1
  74. package/dist/providers/llama-cpp/offload-planner.d.ts +3 -18
  75. package/dist/providers/llama-cpp/offload-planner.js +3 -14
  76. package/dist/providers/llama-cpp/provider.d.ts +8 -0
  77. package/dist/providers/llama-cpp/provider.js +4 -1
  78. package/dist/providers/mlx/provider.js +14 -11
  79. package/dist/providers/mlx/python/batch_processors.py +26 -0
  80. package/dist/providers/mlx/python/gezel_mlx_server.py +88 -20
  81. package/dist/providers/mlx/python/qwen4_ple.py +9 -2
  82. package/dist/providers/mlx/python/response_format.py +374 -0
  83. package/dist/providers/mlx/python/spec_decode.py +5 -0
  84. package/dist/providers/mlx/python/think_budget.py +35 -1
  85. package/dist/providers/mlx/python/tool_grammar.py +62 -3
  86. package/dist/providers/mlx/reasoning-stream.d.ts +20 -2
  87. package/dist/providers/mlx/reasoning-stream.js +77 -16
  88. package/dist/providers/mlx/runtime-diagnostics.d.ts +16 -0
  89. package/dist/providers/mlx/runtime-diagnostics.js +29 -2
  90. package/dist/providers/native/capacity-broker.d.ts +51 -10
  91. package/dist/providers/native/capacity-broker.js +80 -9
  92. package/dist/providers/native/capacity-budget.d.ts +35 -3
  93. package/dist/providers/native/capacity-budget.js +49 -5
  94. package/dist/providers/native/device-capacity.d.ts +1 -1
  95. package/dist/providers/native/device-capacity.js +23 -5
  96. package/dist/providers/native/local-engine-runtime.js +15 -13
  97. package/dist/providers/native/measured-budget.js +2 -0
  98. package/dist/providers/native/supervisor.d.ts +25 -0
  99. package/dist/providers/native/supervisor.js +59 -7
  100. package/dist/remotes/identity.d.ts +1 -0
  101. package/dist/remotes/identity.js +6 -0
  102. package/dist/remotes/pinned-fetch.d.ts +2 -0
  103. package/dist/remotes/pinned-fetch.js +30 -1
  104. package/dist/remotes/types.d.ts +14 -0
  105. package/dist/runtime-lock.js +1 -1
  106. package/dist/system/amdgpu-adapters.d.ts +18 -0
  107. package/dist/system/amdgpu-adapters.js +55 -0
  108. package/dist/system/gpu-carve-out.d.ts +84 -0
  109. package/dist/system/gpu-carve-out.js +111 -0
  110. package/dist/system/memory.d.ts +17 -1
  111. package/dist/system/memory.js +105 -14
  112. package/dist/transformers-cache.d.ts +2 -0
  113. package/dist/transformers-cache.js +6 -3
  114. package/dist/utils/service-worker-entry.d.ts +4 -0
  115. package/dist/utils/service-worker-entry.js +4 -0
  116. package/package.json +14 -10
@@ -0,0 +1,55 @@
1
+ /**
2
+ * The amdgpu devices Linux describes in sysfs, for the carve-out decision in
3
+ * gpu-carve-out.ts — the Linux counterpart of the DXCore adapter listing the
4
+ * device-health helper reads on Windows. Everything read here is
5
+ * world-readable, so the machine service's unprivileged account sees it too.
6
+ */
7
+ import { readFileSync, readdirSync } from 'node:fs';
8
+ import { join } from 'node:path';
9
+ const DRM_ROOT = '/sys/class/drm';
10
+ const AMD_PCI_VENDOR = 0x1002;
11
+ /**
12
+ * One entry per amdgpu device: `dedicatedBytes` from `mem_info_vram_total`,
13
+ * `sharedSystemBytes` from `mem_info_gtt_total` (the system RAM the kernel
14
+ * lets it map). Connector entries (`card0-HDMI-A-1`) and render nodes are
15
+ * skipped. Empty off Linux, without amdgpu, or when sysfs cannot be read.
16
+ */
17
+ export function readAmdgpuAdapters(io = {}) {
18
+ const readFile = io.readFile ?? ((path) => readFileSync(path, 'utf8'));
19
+ const readDir = io.readDir ?? ((path) => readdirSync(path));
20
+ const read = (path) => {
21
+ try {
22
+ return readFile(path).trim();
23
+ }
24
+ catch {
25
+ return undefined;
26
+ }
27
+ };
28
+ let cards;
29
+ try {
30
+ cards = readDir(DRM_ROOT).filter((entry) => /^card\d+$/.test(entry));
31
+ }
32
+ catch {
33
+ return [];
34
+ }
35
+ const adapters = [];
36
+ for (const card of cards) {
37
+ const device = join(DRM_ROOT, card, 'device');
38
+ if (Number.parseInt(read(join(device, 'vendor')) ?? '', 16) !== AMD_PCI_VENDOR)
39
+ continue;
40
+ const vram = byteCount(read(join(device, 'mem_info_vram_total')));
41
+ if (vram === undefined)
42
+ continue;
43
+ const gtt = byteCount(read(join(device, 'mem_info_gtt_total')));
44
+ adapters.push({
45
+ vendorId: AMD_PCI_VENDOR,
46
+ dedicatedBytes: vram,
47
+ ...(gtt !== undefined ? { sharedSystemBytes: gtt } : {}),
48
+ });
49
+ }
50
+ return adapters;
51
+ }
52
+ function byteCount(raw) {
53
+ const value = raw ? Number(raw) : Number.NaN;
54
+ return Number.isSafeInteger(value) && value >= 0 ? value : undefined;
55
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * How much memory firmware set aside for an integrated GPU, if any.
3
+ *
4
+ * Two kinds of integrated GPU report "VRAM" through Vulkan, and they mean
5
+ * opposite things:
6
+ *
7
+ * - A **shared view** (Intel, Snapdragon, most iGPUs): the number is a slice
8
+ * of the same RAM the OS already counts. Adding it double-counts, which is
9
+ * why integrated hosts budget from system RAM alone.
10
+ * - A **carve-out** (an AMD APU's "UMA frame buffer", set in firmware or by
11
+ * AMD's Variable Graphics Memory): memory removed before the OS ever counts
12
+ * RAM. It sits in neither `os.totalmem()` nor any free-RAM reading, and only
13
+ * the GPU can use it. Budgeting from system RAM alone loses it entirely, so
14
+ * on a 16 GB laptop raising the carve-out from 3 GB to 8 GB cut the budget
15
+ * from ~9 GB to ~4 GB.
16
+ *
17
+ * Windows answers directly when the device-health helper can list adapters
18
+ * (DXCore): an integrated adapter's dedicated memory IS its carve-out. Where
19
+ * installed RAM is known too, the two must agree — the carve-out has to fit
20
+ * in the memory that is installed but missing from the OS's count — so one
21
+ * odd driver figure cannot inflate the budget.
22
+ *
23
+ * Without an adapter listing, the GPU's Vulkan-reported memory stands in: a
24
+ * carve-out fits inside that same gap, a shared view is far larger than it
25
+ * (a few hundred MB of firmware reservations is the usual gap without one).
26
+ *
27
+ * Linux answers through the amdgpu driver instead (see
28
+ * {@link linuxGpuCarveOutBytes}). Installed RAM is not readable there without
29
+ * root, and the Vulkan figure cannot stand in: llama.cpp sums every heap of an
30
+ * integrated GPU, and RADV reports an APU's GTT — borrowed system RAM — beside
31
+ * the carve-out.
32
+ *
33
+ * Anything uncertain answers null — integrated GPUs budgeted from system RAM
34
+ * alone, as before.
35
+ */
36
+ export declare function gpuCarveOutBytes(input: {
37
+ /** Physically installed RAM, or null when it could not be read. */
38
+ installedRamBytes: number | null;
39
+ /** RAM the OS reports (`os.totalmem()`). */
40
+ visibleRamBytes: number;
41
+ /** Memory the integrated GPU reports for itself through Vulkan. */
42
+ deviceBytes: number;
43
+ /** Windows' figure for the integrated adapter's own memory, when listed. */
44
+ adapterDedicatedBytes?: number | null;
45
+ }): number | null;
46
+ /**
47
+ * An AMD APU's carve-out on Linux: amdgpu's `mem_info_vram_total`, which on an
48
+ * APU is the frame buffer firmware reserved before the kernel counted RAM, so
49
+ * it is absent from MemTotal. Read only for the one amdgpu device there is —
50
+ * sysfs does not say which of two is integrated — and only when it fits in
51
+ * what Vulkan reports, which for an integrated GPU is the carve-out plus GTT.
52
+ * The caller has already decided the GPU llama.cpp picked is integrated.
53
+ */
54
+ export declare function linuxGpuCarveOutBytes(input: {
55
+ /** amdgpu devices from sysfs (`readAmdgpuAdapters`). */
56
+ adapters: readonly GpuAdapterFacts[];
57
+ visibleRamBytes: number;
58
+ /** Memory the integrated GPU reports for itself through Vulkan. */
59
+ deviceBytes: number;
60
+ }): number | null;
61
+ /**
62
+ * Everything an AMD APU can reach on Linux: its carve-out plus GTT, the
63
+ * window of system RAM amdgpu lets it map (half of RAM on recent kernels
64
+ * unless `ttm.pages_limit` raises it). Past that, allocations fail however
65
+ * much RAM is free. Null without both figures for the one amdgpu device.
66
+ */
67
+ export declare function linuxGpuAddressableBytes(adapters: readonly GpuAdapterFacts[]): number | null;
68
+ /** The part of a helper adapter record (`DeviceGpuAdapter`) this module reads. */
69
+ export interface GpuAdapterFacts {
70
+ name?: string;
71
+ vendorId: number;
72
+ integrated?: boolean;
73
+ dedicatedBytes?: number;
74
+ /** System RAM the adapter may borrow on top of its own (Linux: GTT). */
75
+ sharedSystemBytes?: number;
76
+ }
77
+ /**
78
+ * The adapter Windows lists for the GPU llama.cpp picked: the one with the
79
+ * same name, else the only adapter there is. Null when that is ambiguous —
80
+ * the name-based guesses then stand.
81
+ */
82
+ export declare function matchGpuAdapter<T extends GpuAdapterFacts>(adapters: readonly T[], deviceName: string): T | null;
83
+ /** GPU maker from its PCI vendor id. */
84
+ export declare function gpuVendorFromPciId(vendorId: number): 'amd' | 'intel' | 'nvidia' | undefined;
@@ -0,0 +1,111 @@
1
+ /**
2
+ * How much memory firmware set aside for an integrated GPU, if any.
3
+ *
4
+ * Two kinds of integrated GPU report "VRAM" through Vulkan, and they mean
5
+ * opposite things:
6
+ *
7
+ * - A **shared view** (Intel, Snapdragon, most iGPUs): the number is a slice
8
+ * of the same RAM the OS already counts. Adding it double-counts, which is
9
+ * why integrated hosts budget from system RAM alone.
10
+ * - A **carve-out** (an AMD APU's "UMA frame buffer", set in firmware or by
11
+ * AMD's Variable Graphics Memory): memory removed before the OS ever counts
12
+ * RAM. It sits in neither `os.totalmem()` nor any free-RAM reading, and only
13
+ * the GPU can use it. Budgeting from system RAM alone loses it entirely, so
14
+ * on a 16 GB laptop raising the carve-out from 3 GB to 8 GB cut the budget
15
+ * from ~9 GB to ~4 GB.
16
+ *
17
+ * Windows answers directly when the device-health helper can list adapters
18
+ * (DXCore): an integrated adapter's dedicated memory IS its carve-out. Where
19
+ * installed RAM is known too, the two must agree — the carve-out has to fit
20
+ * in the memory that is installed but missing from the OS's count — so one
21
+ * odd driver figure cannot inflate the budget.
22
+ *
23
+ * Without an adapter listing, the GPU's Vulkan-reported memory stands in: a
24
+ * carve-out fits inside that same gap, a shared view is far larger than it
25
+ * (a few hundred MB of firmware reservations is the usual gap without one).
26
+ *
27
+ * Linux answers through the amdgpu driver instead (see
28
+ * {@link linuxGpuCarveOutBytes}). Installed RAM is not readable there without
29
+ * root, and the Vulkan figure cannot stand in: llama.cpp sums every heap of an
30
+ * integrated GPU, and RADV reports an APU's GTT — borrowed system RAM — beside
31
+ * the carve-out.
32
+ *
33
+ * Anything uncertain answers null — integrated GPUs budgeted from system RAM
34
+ * alone, as before.
35
+ */
36
+ /** Rounding slack between the installed figure, the OS's count, and the device report. */
37
+ const MATCH_TOLERANCE_BYTES = 64 * 1024 ** 2;
38
+ export function gpuCarveOutBytes(input) {
39
+ const { installedRamBytes, visibleRamBytes, deviceBytes } = input;
40
+ const missing = installedRamBytes === null ? null : installedRamBytes - visibleRamBytes;
41
+ const fits = (bytes) => missing === null || bytes <= missing + MATCH_TOLERANCE_BYTES ? bytes : null;
42
+ const adapter = input.adapterDedicatedBytes;
43
+ if (adapter !== undefined && adapter !== null) {
44
+ if (!(adapter > 0))
45
+ return null;
46
+ const agreed = fits(adapter);
47
+ return agreed === null ? null : missing === null ? agreed : Math.min(agreed, missing);
48
+ }
49
+ if (missing === null || !(missing > 0) || !(deviceBytes > 0))
50
+ return null;
51
+ return fits(deviceBytes) === null ? null : Math.min(deviceBytes, missing);
52
+ }
53
+ /**
54
+ * An AMD APU's carve-out on Linux: amdgpu's `mem_info_vram_total`, which on an
55
+ * APU is the frame buffer firmware reserved before the kernel counted RAM, so
56
+ * it is absent from MemTotal. Read only for the one amdgpu device there is —
57
+ * sysfs does not say which of two is integrated — and only when it fits in
58
+ * what Vulkan reports, which for an integrated GPU is the carve-out plus GTT.
59
+ * The caller has already decided the GPU llama.cpp picked is integrated.
60
+ */
61
+ export function linuxGpuCarveOutBytes(input) {
62
+ const carveOut = soleAmdgpuAdapter(input.adapters)?.dedicatedBytes;
63
+ if (carveOut === undefined || carveOut > input.deviceBytes + MATCH_TOLERANCE_BYTES)
64
+ return null;
65
+ return gpuCarveOutBytes({
66
+ installedRamBytes: null,
67
+ visibleRamBytes: input.visibleRamBytes,
68
+ deviceBytes: input.deviceBytes,
69
+ adapterDedicatedBytes: carveOut,
70
+ });
71
+ }
72
+ /**
73
+ * Everything an AMD APU can reach on Linux: its carve-out plus GTT, the
74
+ * window of system RAM amdgpu lets it map (half of RAM on recent kernels
75
+ * unless `ttm.pages_limit` raises it). Past that, allocations fail however
76
+ * much RAM is free. Null without both figures for the one amdgpu device.
77
+ */
78
+ export function linuxGpuAddressableBytes(adapters) {
79
+ const adapter = soleAmdgpuAdapter(adapters);
80
+ const gtt = adapter?.sharedSystemBytes;
81
+ if (adapter?.dedicatedBytes === undefined || gtt === undefined || !(gtt > 0))
82
+ return null;
83
+ return adapter.dedicatedBytes + gtt;
84
+ }
85
+ /** The one amdgpu device, or null when there are none or several. */
86
+ function soleAmdgpuAdapter(adapters) {
87
+ const amd = adapters.filter((adapter) => gpuVendorFromPciId(adapter.vendorId) === 'amd' && adapter.dedicatedBytes !== undefined);
88
+ return amd.length === 1 ? (amd[0] ?? null) : null;
89
+ }
90
+ const nameKey = (name) => name.toLowerCase().replaceAll('(tm)', '').replaceAll('(r)', '').replace(/\s+/g, ' ').trim();
91
+ /**
92
+ * The adapter Windows lists for the GPU llama.cpp picked: the one with the
93
+ * same name, else the only adapter there is. Null when that is ambiguous —
94
+ * the name-based guesses then stand.
95
+ */
96
+ export function matchGpuAdapter(adapters, deviceName) {
97
+ const named = adapters.filter((adapter) => adapter.name !== undefined && nameKey(adapter.name) === nameKey(deviceName));
98
+ if (named.length === 1)
99
+ return named[0] ?? null;
100
+ return adapters.length === 1 ? (adapters[0] ?? null) : null;
101
+ }
102
+ /** GPU maker from its PCI vendor id. */
103
+ export function gpuVendorFromPciId(vendorId) {
104
+ if (vendorId === 0x1002 || vendorId === 0x1022)
105
+ return 'amd';
106
+ if (vendorId === 0x8086)
107
+ return 'intel';
108
+ if (vendorId === 0x10de)
109
+ return 'nvidia';
110
+ return undefined;
111
+ }
@@ -18,6 +18,18 @@ export interface MemoryProfile {
18
18
  gpuVramBytes: number | null;
19
19
  /** Whether the reported GPU pool is separate from or shared with system RAM. */
20
20
  gpuMemoryKind: GpuMemoryKind;
21
+ /**
22
+ * Memory firmware set aside for an integrated GPU, outside
23
+ * `totalRamBytes` (see gpu-carve-out.ts). Present only when measured; a
24
+ * budget built from this profile must pass it on as `reservedGpuBytes`.
25
+ */
26
+ gpuReservedBytes?: number;
27
+ /**
28
+ * The most memory an integrated GPU can reach (Linux amdgpu: carve-out
29
+ * plus GTT; see gpu-carve-out.ts). Present only when measured; a budget
30
+ * built from this profile must pass it on as `gpuAddressableBytes`.
31
+ */
32
+ gpuAddressableBytes?: number;
21
33
  /** Where `usableBytes` comes from. Drives the UI's wording. */
22
34
  source: MemorySource;
23
35
  /**
@@ -71,7 +83,11 @@ export declare function memoryProfileForLlamaGpu(platform: string, totalRamBytes
71
83
  name: string;
72
84
  vendor?: LlamaGpuVendor;
73
85
  memoryKind: LlamaGpuMemoryKind;
74
- }): MemoryProfile;
86
+ },
87
+ /** An integrated GPU's firmware carve-out (gpu-carve-out.ts); ignored otherwise. */
88
+ reservedGpuBytes?: number | null,
89
+ /** The most an integrated GPU can reach (Linux GTT, gpu-carve-out.ts); ignored otherwise. */
90
+ addressableGpuBytes?: number | null): MemoryProfile;
75
91
  /**
76
92
  * Capacity changes only on GPU hot-plug / VM reconfiguration, while the live
77
93
  * strip may poll once a second. Keep the expensive nvidia-smi / llama device
@@ -1,9 +1,11 @@
1
1
  import { execFile } from 'node:child_process';
2
2
  import { freemem, platform as osPlatform, totalmem } from 'node:os';
3
3
  import { promisify } from 'node:util';
4
- import { windowsHeadlessSpawnOptions, } from '@bendyline/gezel/native';
4
+ import { readDeviceSystemFacts, windowsHeadlessSpawnOptions, } from '@bendyline/gezel/native';
5
5
  import { detectLlamaGpuVram, } from '../providers/llama-cpp/devices.js';
6
6
  import { autoDetectBudgetBytes, computeCapacityBudget, setDetectedGpuVramBytes, } from '../providers/native/capacity-budget.js';
7
+ import { readAmdgpuAdapters } from './amdgpu-adapters.js';
8
+ import { gpuCarveOutBytes, gpuVendorFromPciId, linuxGpuAddressableBytes, linuxGpuCarveOutBytes, matchGpuAdapter, } from './gpu-carve-out.js';
7
9
  const exec = promisify(execFile);
8
10
  let cachedMemoryProfile = null;
9
11
  let pendingMemoryProfile = null;
@@ -72,17 +74,53 @@ export async function detectMemoryProfile() {
72
74
  // `--list-devices` — the same number the MoE offload planner uses. Use it
73
75
  // so a Radeon box gets a real GPU budget (and the "runs on CPU" copy stops
74
76
  // lying) instead of the 50%-of-RAM CPU fallback below.
75
- const gpu = await detectLlamaGpuVram();
76
- if (gpu !== null) {
77
- const profile = memoryProfileForLlamaGpu(plat, totalRam, gpu);
77
+ const probed = await detectLlamaGpuVram();
78
+ if (probed !== null) {
79
+ // Windows' own description of the same GPU, when the helper can list it,
80
+ // replaces the guesses made from its name.
81
+ const facts = await systemFacts();
82
+ const adapter = facts?.adapters ? matchGpuAdapter(facts.adapters, probed.name) : null;
83
+ const vendor = (adapter ? gpuVendorFromPciId(adapter.vendorId) : undefined) ?? probed.vendor;
84
+ const gpu = {
85
+ ...probed,
86
+ memoryKind: adapter?.integrated === undefined
87
+ ? probed.memoryKind
88
+ : adapter.integrated
89
+ ? 'integrated'
90
+ : 'discrete',
91
+ ...(vendor ? { vendor } : {}),
92
+ };
93
+ const reservedGpuBytes = gpu.memoryKind !== 'integrated'
94
+ ? null
95
+ : plat === 'win32'
96
+ ? gpuCarveOutBytes({
97
+ installedRamBytes: facts?.installedRamBytes ?? null,
98
+ visibleRamBytes: totalRam,
99
+ deviceBytes: gpu.vramBytes,
100
+ adapterDedicatedBytes: adapter?.integrated ? (adapter.dedicatedBytes ?? null) : null,
101
+ })
102
+ : plat === 'linux' && gpu.vendor === 'amd'
103
+ ? linuxGpuCarveOutBytes({
104
+ adapters: amdgpuAdapters(),
105
+ visibleRamBytes: totalRam,
106
+ deviceBytes: gpu.vramBytes,
107
+ })
108
+ : null;
109
+ // Linux bounds an APU by GTT as well; Windows' shared-memory limit is
110
+ // not read, so the RAM share stands there as before.
111
+ const addressableGpuBytes = gpu.memoryKind === 'integrated' && plat === 'linux' && gpu.vendor === 'amd'
112
+ ? linuxGpuAddressableBytes(amdgpuAdapters())
113
+ : null;
114
+ const profile = memoryProfileForLlamaGpu(plat, totalRam, gpu, reservedGpuBytes, addressableGpuBytes);
78
115
  // The ambient capacity broker accepts only a VRAM byte count. Publishing
79
116
  // an integrated adapter's shared allocation there would add it to RAM a
80
- // second time, so publish null for that host class.
81
- setDetectedGpuVramBytes(profile.gpuMemoryKind === 'integrated' ? null : gpu.vramBytes, profile.gpuMemoryKind === 'integrated' || profile.gpuMemoryKind === 'unified');
82
- // Vendor comes from the device name in the SAME probe (e.g. "AMD Radeon
83
- // …" / "Intel Arc …"), so the label always matches the card we sized —
84
- // never "AMD" for an Intel GPU. Undefined for unrecognized names → the
85
- // UI shows a generic "GPU".
117
+ // second time, so publish null for that host class — and its carve-out,
118
+ // which is the one part of it that is not RAM, separately.
119
+ setDetectedGpuVramBytes(profile.gpuMemoryKind === 'integrated' ? null : gpu.vramBytes, profile.gpuMemoryKind === 'integrated' || profile.gpuMemoryKind === 'unified', profile.gpuReservedBytes ?? null, profile.gpuAddressableBytes ?? null);
120
+ // Vendor comes from the SAME probe — the matched adapter's PCI id, else
121
+ // the device name (e.g. "AMD Radeon …" / "Intel Arc …") — so the label
122
+ // always matches the card we sized, never "AMD" for an Intel GPU.
123
+ // Undefined for unrecognized devices → the UI shows a generic "GPU".
86
124
  return profile;
87
125
  }
88
126
  setDetectedGpuVramBytes(null);
@@ -105,7 +143,11 @@ export async function detectMemoryProfile() {
105
143
  * double-counting an integrated adapter's shared allocation. Exported as the
106
144
  * pure policy seam for regression tests and diagnostics tooling.
107
145
  */
108
- export function memoryProfileForLlamaGpu(platform, totalRamBytes, gpu) {
146
+ export function memoryProfileForLlamaGpu(platform, totalRamBytes, gpu,
147
+ /** An integrated GPU's firmware carve-out (gpu-carve-out.ts); ignored otherwise. */
148
+ reservedGpuBytes = null,
149
+ /** The most an integrated GPU can reach (Linux GTT, gpu-carve-out.ts); ignored otherwise. */
150
+ addressableGpuBytes = null) {
109
151
  const ratioLooksUnified = gpu.vramBytes >= totalRamBytes * 0.75;
110
152
  const gpuMemoryKind = gpu.memoryKind === 'integrated'
111
153
  ? 'integrated'
@@ -115,21 +157,37 @@ export function memoryProfileForLlamaGpu(platform, totalRamBytes, gpu) {
115
157
  ? 'unified'
116
158
  : 'unknown';
117
159
  const shared = gpuMemoryKind === 'integrated' || gpuMemoryKind === 'unified';
160
+ const reserved = gpuMemoryKind === 'integrated' && reservedGpuBytes !== null && reservedGpuBytes > 0
161
+ ? reservedGpuBytes
162
+ : null;
163
+ const addressable = gpuMemoryKind === 'integrated' && addressableGpuBytes !== null && addressableGpuBytes > 0
164
+ ? addressableGpuBytes
165
+ : null;
118
166
  const budget = computeCapacityBudget({
119
167
  systemRamBytes: totalRamBytes,
120
168
  gpuVramBytes: gpu.vramBytes,
121
169
  unifiedMemory: shared,
170
+ reservedGpuBytes: reserved,
171
+ gpuAddressableBytes: addressable,
122
172
  });
123
173
  return {
124
174
  platform,
125
175
  totalRamBytes,
126
176
  gpuVramBytes: gpu.vramBytes,
127
177
  gpuMemoryKind,
178
+ ...(reserved !== null ? { gpuReservedBytes: reserved } : {}),
179
+ ...(addressable !== null ? { gpuAddressableBytes: addressable } : {}),
128
180
  source: gpuMemoryKind === 'integrated' ? 'gpu-integrated' : 'gpu-vulkan',
129
181
  // A consumer iGPU is effectively system-memory-paced. Keep the same
130
182
  // conservative fast-fit ceiling as CPU fallback while the broker may
131
- // admit up to its single-pool unified ceiling.
132
- usableBytes: gpuMemoryKind === 'integrated' ? Math.floor(totalRamBytes * 0.5) : budget.fastBytes,
183
+ // admit up to its single-pool unified ceiling. A carve-out adds what the
184
+ // budget credits from it (`vramBytes`), since none of it is in RAM. Where
185
+ // GTT is known, what the GPU can reach at all (`fastBytes`) bounds both.
186
+ usableBytes: gpuMemoryKind !== 'integrated'
187
+ ? budget.fastBytes
188
+ : addressable !== null
189
+ ? Math.min(Math.floor(totalRamBytes * 0.5) + budget.vramBytes, budget.fastBytes)
190
+ : Math.floor(totalRamBytes * 0.5) + budget.vramBytes,
133
191
  budgetBytes: budget.budgetBytes,
134
192
  ...(gpu.vendor ? { gpuVendor: gpu.vendor } : {}),
135
193
  };
@@ -487,12 +545,45 @@ function isKnownUnifiedNvidiaDevice(name) {
487
545
  const normalized = name.toLowerCase();
488
546
  return /\bgb10\b/.test(normalized) || normalized.includes('dgx spark');
489
547
  }
548
+ /**
549
+ * A hybrid-graphics laptop powers its discrete GPU down when idle, and the
550
+ * first `nvidia-smi` after that waits for it to wake: 2.25–2.36 s on an RTX
551
+ * 5070 Ti laptop (2026-10-15), against ~0.1 s warm. The old 2 s limit read
552
+ * that as "no NVIDIA GPU", so the machine was sized as CPU-only for the next
553
+ * minute and offered a 2.4 GB model with a 12 GB card idle beside it. Startup
554
+ * never awaits this probe, so the margin only costs a caller that asks for the
555
+ * profile while the driver is truly hung.
556
+ */
557
+ const NVIDIA_MEMORY_PROBE_TIMEOUT_MS = 10_000;
558
+ let cachedSystemFacts = null;
559
+ /**
560
+ * Static facts from the bundled device-health helper — installed RAM and
561
+ * Windows' list of GPUs — read once per process: neither changes without a
562
+ * reboot. Null off Windows. Empty without the helper, or from one that
563
+ * predates the `system` command, which leaves every decision on the
564
+ * name-based guesses it made before.
565
+ */
566
+ function systemFacts() {
567
+ cachedSystemFacts ??= readSystemFacts();
568
+ return cachedSystemFacts;
569
+ }
570
+ async function readSystemFacts() {
571
+ if (osPlatform() !== 'win32')
572
+ return null;
573
+ return readDeviceSystemFacts();
574
+ }
575
+ let cachedAmdgpuAdapters = null;
576
+ /** amdgpu devices from sysfs, read once per process like the Windows facts. */
577
+ function amdgpuAdapters() {
578
+ cachedAmdgpuAdapters ??= readAmdgpuAdapters();
579
+ return cachedAmdgpuAdapters;
580
+ }
490
581
  async function probeNvidiaMemory() {
491
582
  try {
492
583
  const { stdout } = await exec('nvidia-smi', ['--query-gpu=name,memory.total', '--format=csv,noheader,nounits'],
493
584
  // Keep this short-lived console probe invisible without detaching it
494
585
  // from the daemon that owns and awaits it.
495
- { timeout: 2000, ...windowsHeadlessSpawnOptions() });
586
+ { timeout: NVIDIA_MEMORY_PROBE_TIMEOUT_MS, ...windowsHeadlessSpawnOptions() });
496
587
  return parseNvidiaMemoryProbe(stdout);
497
588
  }
498
589
  catch {
@@ -68,6 +68,8 @@ export interface ModelFileSource {
68
68
  remoteHost: string;
69
69
  remotePathTemplate: string;
70
70
  fetchImpl?: typeof fetch;
71
+ /** Abandons the download (shutdown); the partial file is removed as on any failure. */
72
+ signal?: AbortSignal;
71
73
  }
72
74
  /**
73
75
  * Make sure a pinned model file sits in the transformers cache whole.
@@ -230,11 +230,12 @@ async function sweepPartialDownloads(target) {
230
230
  .filter((name) => name.startsWith(prefix))
231
231
  .map((name) => rm(join(dirname(target), name), { force: true }).catch(() => { })));
232
232
  }
233
- async function downloadVerified(url, target, pinned, fetchImpl) {
233
+ async function downloadVerified(url, target, pinned, fetchImpl, signal) {
234
234
  const partial = `${target}${PARTIAL_DOWNLOAD_INFIX}${process.pid}-${randomUUID().slice(0, 8)}`;
235
235
  let handle;
236
236
  try {
237
- const response = await fetchImpl(url);
237
+ signal?.throwIfAborted();
238
+ const response = await fetchImpl(url, signal ? { signal } : undefined);
238
239
  if (!response.ok || !response.body) {
239
240
  throw new Error(`HTTP ${response.status} downloading ${pinned.file}`);
240
241
  }
@@ -245,6 +246,8 @@ async function downloadVerified(url, target, pinned, fetchImpl) {
245
246
  const hash = createHash('sha256');
246
247
  let received = 0;
247
248
  for await (const chunk of response.body) {
249
+ // A fetchImpl that ignores the signal still stops at the next chunk.
250
+ signal?.throwIfAborted();
248
251
  hash.update(chunk);
249
252
  received += chunk.byteLength;
250
253
  let offset = 0;
@@ -302,7 +305,7 @@ export async function ensureVerifiedModelFile(cacheDir, modelId, pinned, source)
302
305
  }
303
306
  await rm(marker, { force: true });
304
307
  await rm(target, { force: true });
305
- await downloadVerified(remoteFileUrl(source, modelId, pinned), target, pinned, source.fetchImpl ?? fetch);
308
+ await downloadVerified(remoteFileUrl(source, modelId, pinned), target, pinned, source.fetchImpl ?? fetch, source.signal);
306
309
  await writeMarker(marker, target, pinned.digest);
307
310
  return 'downloaded';
308
311
  }
@@ -16,6 +16,10 @@ export declare const SERVICE_WORKER_ENTRIES: {
16
16
  readonly 'static-index': {
17
17
  readonly built: "index-store/static-index-worker.js";
18
18
  };
19
+ readonly 'dashboard-composite': {
20
+ readonly built: "ambient/dashboard-composite-worker.js";
21
+ readonly source: "ambient/dashboard-composite-worker.ts";
22
+ };
19
23
  readonly 'document-convert': {
20
24
  readonly built: "index-store/convert-worker.js";
21
25
  readonly source: "index-store/convert-worker.ts";
@@ -13,6 +13,10 @@ export const SERVICE_WORKER_ENTRIES = {
13
13
  embeddings: { built: 'memory/embed-worker.js' },
14
14
  'image-embeddings': { built: 'memory/image-embed-worker.js' },
15
15
  'static-index': { built: 'index-store/static-index-worker.js' },
16
+ 'dashboard-composite': {
17
+ built: 'ambient/dashboard-composite-worker.js',
18
+ source: 'ambient/dashboard-composite-worker.ts',
19
+ },
16
20
  'document-convert': {
17
21
  built: 'index-store/convert-worker.js',
18
22
  source: 'index-store/convert-worker.ts',
package/package.json CHANGED
@@ -1,16 +1,20 @@
1
1
  {
2
2
  "name": "@bendyline/gezel-inference",
3
- "version": "1.0.0",
4
- "description": "Shared on-device inference, model management and knowledge runtime for Gezel hosts.",
3
+ "version": "1.0.2",
4
+ "description": "Gezel's on-device inference runtime: native engines, local model management, embeddings, knowledge search, and OpenAI-compatible routes.",
5
5
  "license": "MIT",
6
6
  "keywords": [
7
7
  "gezel",
8
8
  "ai",
9
- "agents",
10
9
  "local-first",
11
- "daemon",
12
- "server",
13
- "llm"
10
+ "llm",
11
+ "inference",
12
+ "on-device",
13
+ "local-ai",
14
+ "llama.cpp",
15
+ "mlx",
16
+ "embeddings",
17
+ "openai-compatible"
14
18
  ],
15
19
  "homepage": "https://github.com/bendyline/gezel",
16
20
  "bugs": {
@@ -69,10 +73,10 @@
69
73
  "!dist/**/__pycache__"
70
74
  ],
71
75
  "dependencies": {
72
- "@bendyline/gezel": "1.2.5",
73
- "@bendyline/gezel-catalog": "1.2.5",
74
- "@bendyline/gezel-client": "1.2.5",
75
- "@bendyline/gezel-knowledge": "1.2.5",
76
+ "@bendyline/gezel": "1.2.7",
77
+ "@bendyline/gezel-catalog": "1.2.7",
78
+ "@bendyline/gezel-client": "1.2.7",
79
+ "@bendyline/gezel-knowledge": "1.2.7",
76
80
  "hono": "4.13.9",
77
81
  "tar": "7.5.21",
78
82
  "undici": "8.11.2",