@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.
- package/dist/catalog/chat-models/ge/gemma4-12b-q4/manifest.json +46 -3
- package/dist/catalog/chat-models/ge/gemma4-12b-q4/versions/1.2.1/manifest.json +141 -0
- package/dist/catalog/chat-models/ge/gemma4-e4b-q4/manifest.json +5 -0
- package/dist/catalog/chat-models/index.json +1 -1
- package/dist/catalog/chat-models/or/ornith1.5-9b-q4/manifest.json +5 -0
- package/dist/catalog/chat-models/or/ornith1.5-9b-q8/manifest.json +5 -0
- package/dist/catalog/chat-models/qw/qwen3.5-9b-q4/manifest.json +5 -0
- package/dist/catalog/chat-models/qw/qwen3.8-27b-q4/manifest.json +5 -0
- package/dist/catalog/chat-models/qw/qwen3.8-flash-next-q2/manifest.json +24 -7
- package/dist/catalog/chat-models/qw/qwen3.8-flash-next-q2/versions/1.0.3/manifest.json +47 -0
- package/dist/catalog/chat-models/raw-index.json +1 -1
- package/dist/catalog/provenance.json +1 -1
- package/dist/engines/electron-native-reuse.d.ts +5 -4
- package/dist/engines/electron-native-reuse.js +1 -1
- package/dist/engines/native-file-manifest.json +12344 -698
- package/dist/engines/native-manifest.d.ts +4 -4
- package/dist/engines/native-manifest.js +40 -36
- package/dist/engines/resolver.d.ts +18 -17
- package/dist/engines/resolver.js +36 -1
- package/dist/http/model-inventory-cache.d.ts +1 -1
- package/dist/http/routes/engines.js +3 -3
- package/dist/http/routes/v1-images.d.ts +2 -1
- package/dist/http/routes/v1-images.js +36 -9
- package/dist/knowledge/manager.d.ts +3 -27
- package/dist/knowledge/manager.js +8 -364
- package/dist/knowledge/unified-search.d.ts +61 -0
- package/dist/knowledge/unified-search.js +373 -0
- package/dist/models/catalog-drift.d.ts +2 -0
- package/dist/models/catalog-drift.js +1 -1
- package/dist/models/catalog-payload-identity.d.ts +2 -2
- package/dist/models/catalog-payload-identity.js +10 -5
- package/dist/providers/active-install-message.js +1 -1
- package/dist/providers/catalog-model-config.d.ts +28 -0
- package/dist/providers/ds4/build-provider.js +35 -13
- package/dist/providers/ds4/residency.d.ts +77 -0
- package/dist/providers/ds4/residency.js +87 -6
- package/dist/providers/image/sd-cpp.js +19 -11
- package/dist/providers/llama-cpp/backend-health.d.ts +3 -2
- package/dist/providers/llama-cpp/backend-health.js +6 -3
- package/dist/providers/llama-cpp/backend-quarantine.js +1 -1
- package/dist/providers/llama-cpp/build-provider.js +168 -29
- package/dist/providers/llama-cpp/devices.js +4 -1
- package/dist/providers/llama-cpp/engine-flags.d.ts +8 -1
- package/dist/providers/llama-cpp/engine-flags.js +13 -8
- package/dist/providers/llama-cpp/gpu-startup-watch.d.ts +4 -4
- package/dist/providers/llama-cpp/gpu-startup-watch.js +7 -5
- package/dist/providers/llama-cpp/models.d.ts +10 -0
- package/dist/providers/llama-cpp/models.js +64 -10
- package/dist/providers/llama-cpp/offload-planner.d.ts +6 -0
- package/dist/providers/llama-cpp/offload-planner.js +1 -1
- package/dist/providers/llama-cpp/spec-draft.d.ts +45 -23
- package/dist/providers/llama-cpp/spec-draft.js +113 -24
- package/dist/providers/mlx/async-file-handoff.d.ts +9 -1
- package/dist/providers/mlx/async-file-handoff.js +32 -4
- package/dist/providers/mlx/provider.js +28 -16
- package/dist/providers/mlx/python/spec_decode.py +23 -2
- package/dist/providers/mlx/python/tool_args_json.py +59 -21
- package/dist/providers/mlx/runtime-diagnostics.d.ts +6 -0
- package/dist/providers/mlx/runtime-diagnostics.js +18 -1
- package/dist/providers/mlx/tool-call-protocol.d.ts +4 -3
- package/dist/providers/mlx/tool-call-protocol.js +4 -3
- package/dist/providers/mlx/venv.d.ts +19 -1
- package/dist/providers/mlx/venv.js +21 -2
- package/dist/providers/native/device-capacity-ledger.d.ts +4 -2
- package/dist/providers/native/device-capacity-ledger.js +35 -20
- package/dist/providers/native/device-capacity.d.ts +4 -0
- package/dist/providers/native/device-capacity.js +5 -0
- package/dist/providers/native/local-engine-runtime.d.ts +16 -1
- package/dist/providers/native/local-engine-runtime.js +142 -39
- package/dist/providers/native/process-identity.d.ts +38 -0
- package/dist/providers/native/process-identity.js +86 -0
- package/dist/providers/native/supervisor.d.ts +8 -1
- package/dist/providers/native/supervisor.js +16 -7
- package/dist/providers/types.d.ts +4 -0
- package/dist/remotes/types.d.ts +1 -1
- package/dist/runtime-lock.d.ts +3 -27
- package/dist/runtime-lock.js +7 -80
- package/package.json +4 -4
|
@@ -22,13 +22,17 @@ chunk by chunk. Everything else (ints, floats, bools, objects, arrays) is
|
|
|
22
22
|
small and needs the whole value for `_convert_param_value`-style coercion, so
|
|
23
23
|
those are buffered per-parameter and emitted when their `</parameter>` lands.
|
|
24
24
|
|
|
25
|
-
Semantics are copied from mlx_vlm 0.
|
|
26
|
-
stay identical; `
|
|
27
|
-
every case against the reference parser. In particular:
|
|
25
|
+
Semantics are copied from mlx_vlm 0.7.12 `tools/parsers/qwen3_coder.py` and
|
|
26
|
+
must stay identical; `tool_args_json_test.py` asserts that by differential-
|
|
27
|
+
testing every case against the reference parser. In particular:
|
|
28
28
|
|
|
29
29
|
* exactly ONE leading and ONE trailing "\\n" is stripped from a value
|
|
30
|
-
* a literal "null" (any case) becomes JSON null
|
|
30
|
+
* a literal "null" (any case) becomes JSON null, except in a declared
|
|
31
|
+
string parameter, which keeps the text unless the schema says
|
|
32
|
+
`"nullable": true`
|
|
31
33
|
* a parameter absent from the schema is treated as a string
|
|
34
|
+
* a declared parameter without a type decodes JSON objects and arrays and
|
|
35
|
+
keeps anything else as text
|
|
32
36
|
* unknown/odd types fall through to `ast.literal_eval`, as upstream does
|
|
33
37
|
|
|
34
38
|
Chunk boundaries are the hazard throughout: `</parameter>`, `</function>` and
|
|
@@ -68,29 +72,59 @@ def arguments_config(func_name: str, tools: Optional[Any]) -> Dict[str, Any]:
|
|
|
68
72
|
return {}
|
|
69
73
|
|
|
70
74
|
|
|
75
|
+
def _declared_string(param: Any) -> bool:
|
|
76
|
+
return (
|
|
77
|
+
isinstance(param, dict)
|
|
78
|
+
and isinstance(param.get("type"), str)
|
|
79
|
+
and param["type"].strip().lower() in _STRING_TYPES
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
71
83
|
def is_string_param(param_name: str, param_config: Dict[str, Any]) -> bool:
|
|
72
84
|
"""Can this parameter stream, or must its value be buffered whole?
|
|
73
85
|
|
|
74
86
|
Unknown parameters count as strings — upstream returns the raw value when
|
|
75
|
-
the schema has no entry, and raw-string is the streamable case.
|
|
87
|
+
the schema has no entry, and raw-string is the streamable case. A declared
|
|
88
|
+
parameter without a type is buffered: its value may decode to an object.
|
|
76
89
|
"""
|
|
77
|
-
|
|
78
|
-
if not param:
|
|
79
|
-
return True
|
|
80
|
-
declared = param.get("type")
|
|
81
|
-
if declared is None:
|
|
90
|
+
if param_name not in param_config:
|
|
82
91
|
return True
|
|
83
|
-
|
|
92
|
+
param = param_config[param_name]
|
|
93
|
+
if not isinstance(param, dict) or "type" not in param:
|
|
94
|
+
return False
|
|
95
|
+
return str(param["type"]).strip().lower() in _STRING_TYPES
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def null_text_is_none(param_name: str, param_config: Dict[str, Any]) -> bool:
|
|
99
|
+
"""Whether a complete value of "null" is JSON null for this parameter.
|
|
100
|
+
|
|
101
|
+
A declared string keeps the text unless the schema marks it nullable;
|
|
102
|
+
every other parameter, unknown ones included, reads it as null.
|
|
103
|
+
"""
|
|
104
|
+
param = param_config.get(param_name)
|
|
105
|
+
if _declared_string(param):
|
|
106
|
+
return param.get("nullable") is True
|
|
107
|
+
return True
|
|
84
108
|
|
|
85
109
|
|
|
86
110
|
def convert_param_value(value: str, param_name: str, param_config: Dict[str, Any]) -> Any:
|
|
87
111
|
"""Coerce a complete value, mirroring `_convert_param_value`."""
|
|
112
|
+
param = param_config.get(param_name)
|
|
113
|
+
if _declared_string(param):
|
|
114
|
+
if value.lower() == "null" and param.get("nullable") is True:
|
|
115
|
+
return None
|
|
116
|
+
return value
|
|
88
117
|
if value.lower() == "null":
|
|
89
118
|
return None
|
|
90
|
-
|
|
91
|
-
if not param:
|
|
119
|
+
if param_name not in param_config:
|
|
92
120
|
return value
|
|
93
|
-
|
|
121
|
+
if not isinstance(param, dict) or "type" not in param:
|
|
122
|
+
try:
|
|
123
|
+
decoded = json.loads(value)
|
|
124
|
+
except json.JSONDecodeError:
|
|
125
|
+
return value
|
|
126
|
+
return decoded if isinstance(decoded, (dict, list)) else value
|
|
127
|
+
param_type = str(param["type"]).strip().lower()
|
|
94
128
|
if param_type in _STRING_TYPES:
|
|
95
129
|
return value
|
|
96
130
|
if param_type.startswith(("int", "uint", "long", "short", "unsigned")):
|
|
@@ -194,10 +228,10 @@ class ToolArgsJsonEncoder:
|
|
|
194
228
|
self._leading_stripped = False
|
|
195
229
|
self._opened_quote = False
|
|
196
230
|
# NOTE: the opening quote is deliberately NOT emitted yet.
|
|
197
|
-
#
|
|
198
|
-
#
|
|
199
|
-
#
|
|
200
|
-
#
|
|
231
|
+
# A whole value of "null" is JSON null for an unknown or
|
|
232
|
+
# nullable string parameter (`null_text_is_none`) — and by
|
|
233
|
+
# then a quote would already be on the wire. Hold the first
|
|
234
|
+
# few bytes.
|
|
201
235
|
continue
|
|
202
236
|
|
|
203
237
|
end = self._buf.find(_PARAM_CLOSE)
|
|
@@ -228,8 +262,8 @@ class ToolArgsJsonEncoder:
|
|
|
228
262
|
# * strip exactly one leading newline
|
|
229
263
|
# * strip exactly one trailing newline (so the final newline is
|
|
230
264
|
# held until we know whether anything follows it)
|
|
231
|
-
# * a complete value of "null"
|
|
232
|
-
# the opening quote waits until the value is longer than
|
|
265
|
+
# * a complete value of "null" can be JSON null, not a string,
|
|
266
|
+
# so the opening quote waits until the value is longer than
|
|
233
267
|
# "null" or the parameter closes
|
|
234
268
|
if not self._leading_stripped:
|
|
235
269
|
if chunk.startswith("\n"):
|
|
@@ -241,7 +275,11 @@ class ToolArgsJsonEncoder:
|
|
|
241
275
|
self._value_buf += chunk
|
|
242
276
|
if end < 0 and len(self._value_buf) <= 4:
|
|
243
277
|
break
|
|
244
|
-
if
|
|
278
|
+
if (
|
|
279
|
+
end >= 0
|
|
280
|
+
and self._value_buf.lower() == "null"
|
|
281
|
+
and null_text_is_none(self._value_name, self.param_config)
|
|
282
|
+
):
|
|
245
283
|
out.append(f"{json.dumps(self._value_name)}:null")
|
|
246
284
|
self._in_value = False
|
|
247
285
|
continue
|
|
@@ -38,6 +38,12 @@ export declare function buildPreFirstByteAbortMessage(lastPrefill: {
|
|
|
38
38
|
} | null): string;
|
|
39
39
|
/** Build the user-facing message for an engine stream that died mid-turn. */
|
|
40
40
|
export declare function buildMidStreamDropMessage(charsReceived: number, plannedStop: boolean, engineStillRunning?: boolean): string;
|
|
41
|
+
/**
|
|
42
|
+
* Prompt characters one attached image stands for. The sidecar bounds images
|
|
43
|
+
* to 1024px, so their patch tokens are bounded too, and base64 bytes are never
|
|
44
|
+
* read as language tokens.
|
|
45
|
+
*/
|
|
46
|
+
export declare const MLX_IMAGE_RESERVED_CHARS = 8192;
|
|
41
47
|
/** Estimate request tokens from message bodies and serialized tool schemas. */
|
|
42
48
|
export declare function estimatePromptTokens(messages: unknown, tools: unknown): number;
|
|
43
49
|
export declare const PRE_FIRST_BYTE_BASE_MS = 300000;
|
|
@@ -146,11 +146,28 @@ export function buildMidStreamDropMessage(charsReceived, plannedStop, engineStil
|
|
|
146
146
|
}
|
|
147
147
|
return `[Mac AI] the on-device engine dropped the connection mid-turn (${got}). This usually means the mlx server crashed, ran out of memory, or was restarted while the turn was streaming — this turn's work was lost. Retry the turn; if it keeps happening, restart the engine in Settings → On-device.`;
|
|
148
148
|
}
|
|
149
|
+
/**
|
|
150
|
+
* Prompt characters one attached image stands for. The sidecar bounds images
|
|
151
|
+
* to 1024px, so their patch tokens are bounded too, and base64 bytes are never
|
|
152
|
+
* read as language tokens.
|
|
153
|
+
*/
|
|
154
|
+
export const MLX_IMAGE_RESERVED_CHARS = 8192;
|
|
149
155
|
/** Estimate request tokens from message bodies and serialized tool schemas. */
|
|
150
156
|
export function estimatePromptTokens(messages, tools) {
|
|
151
157
|
let chars = 0;
|
|
152
158
|
try {
|
|
153
|
-
|
|
159
|
+
// Counted as base64, one generated picture read as ~1.1M tokens: an
|
|
160
|
+
// image gezel's six renders and reads put the estimate past 6M while the
|
|
161
|
+
// engine prefilled 14K (qwen3.8-27b, 2026-11-04).
|
|
162
|
+
let images = 0;
|
|
163
|
+
const text = JSON.stringify(messages, (key, value) => {
|
|
164
|
+
if (key === 'images' && Array.isArray(value)) {
|
|
165
|
+
images += value.length;
|
|
166
|
+
return undefined;
|
|
167
|
+
}
|
|
168
|
+
return value;
|
|
169
|
+
});
|
|
170
|
+
chars += (text?.length ?? 0) + images * MLX_IMAGE_RESERVED_CHARS;
|
|
154
171
|
}
|
|
155
172
|
catch {
|
|
156
173
|
/* circular or unstringifiable */
|
|
@@ -39,9 +39,10 @@ export declare function applyCallableToolGrammar(body: Record<string, unknown>,
|
|
|
39
39
|
*
|
|
40
40
|
* This deliberately checks presence only. A present-but-wrongly-typed value
|
|
41
41
|
* remains the MCP validator's job; the MLX Hermes grammar promises only that
|
|
42
|
-
* every declared required key is emitted before
|
|
43
|
-
* missing key
|
|
44
|
-
*
|
|
42
|
+
* every declared required key is emitted before a constrained call can close.
|
|
43
|
+
* A missing key alone does not establish a decoder fault: calls salvaged from
|
|
44
|
+
* ordinary prose were not subject to that grammar. The caller must preserve
|
|
45
|
+
* their origin when deciding whether to disable speculative decoding.
|
|
45
46
|
*/
|
|
46
47
|
export declare function missingTopLevelRequiredToolArgs(schema: Record<string, unknown> | undefined, args: Record<string, unknown>): string[];
|
|
47
48
|
export declare function hermesRequiredArgGrammarRequested(body: Record<string, unknown>): boolean;
|
|
@@ -59,9 +59,10 @@ export function applyCallableToolGrammar(body, bridges) {
|
|
|
59
59
|
*
|
|
60
60
|
* This deliberately checks presence only. A present-but-wrongly-typed value
|
|
61
61
|
* remains the MCP validator's job; the MLX Hermes grammar promises only that
|
|
62
|
-
* every declared required key is emitted before
|
|
63
|
-
* missing key
|
|
64
|
-
*
|
|
62
|
+
* every declared required key is emitted before a constrained call can close.
|
|
63
|
+
* A missing key alone does not establish a decoder fault: calls salvaged from
|
|
64
|
+
* ordinary prose were not subject to that grammar. The caller must preserve
|
|
65
|
+
* their origin when deciding whether to disable speculative decoding.
|
|
65
66
|
*/
|
|
66
67
|
export function missingTopLevelRequiredToolArgs(schema, args) {
|
|
67
68
|
const required = schema?.required;
|
|
@@ -78,9 +78,27 @@
|
|
|
78
78
|
* instead of consuming the Metal working set. Do not treat the library pin
|
|
79
79
|
* alone as proof that every community conversion is correct or fits.
|
|
80
80
|
*
|
|
81
|
+
* 0.7.12 (2026-11-05): the project moved to Neywa-Labs/mlx-vlm; same PyPI
|
|
82
|
+
* name. It resolves mlx 0.32.4 (mlx-audio pins it exactly) and transformers
|
|
83
|
+
* 5.22. Every private `speculative.mtp` helper the assisted round loop calls
|
|
84
|
+
* is unchanged for Qwen 3.5; Gemma 4 targets now rebuild the drafter's shared
|
|
85
|
+
* KV from committed caches, which `shared_kv_after_round` follows. Its
|
|
86
|
+
* qwen3_coder parser keeps "null" as text in a string parameter, which
|
|
87
|
+
* `tool_args_json.py` now mirrors. Verified on an M5 Max against 0.7.1
|
|
88
|
+
* through the real sidecar: gemma4-e4b and qwen3.8-27b + MTP drafter render
|
|
89
|
+
* identical prompt token counts and identical tool-call, JSON and vision
|
|
90
|
+
* output; 0.7.1-written disk caches load and extend (Gemma 4 rotating,
|
|
91
|
+
* Qwen 3.5 hybrid). Greedy MTP matches greedy no-spec on code at 0-30k
|
|
92
|
+
* tokens; on prose both versions flip bf16 one-step ties (top-2 gap 0.125
|
|
93
|
+
* nats), which two non-speculative decode paths also disagree on, so prose
|
|
94
|
+
* is not an exactness probe. Clean A/B/B/A decode on qwen3.8-27b-q4: MTP on
|
|
95
|
+
* the vlm tower at parity to 10k and +19% at 50k; vlm tower without MTP 2.0x
|
|
96
|
+
* at 50k (7.3 -> 14.8 tok/s), now level with the mlx_lm tower; mlx_lm tower
|
|
97
|
+
* 0.94x at 0-10k, within run-to-run noise.
|
|
98
|
+
*
|
|
81
99
|
* Keep upgrades explicit and re-sweep the MLX catalog whenever this pin
|
|
82
100
|
* moves. */
|
|
83
|
-
export declare const MLX_DEFAULT_PACKAGE_SPEC = "mlx-vlm==0.7.
|
|
101
|
+
export declare const MLX_DEFAULT_PACKAGE_SPEC = "mlx-vlm==0.7.12";
|
|
84
102
|
/** Venv name passed to `UvRuntime.ensureVenv` (and its dir basename). */
|
|
85
103
|
export declare const MLX_VENV_NAME = "mlx";
|
|
86
104
|
/**
|
|
@@ -78,9 +78,27 @@
|
|
|
78
78
|
* instead of consuming the Metal working set. Do not treat the library pin
|
|
79
79
|
* alone as proof that every community conversion is correct or fits.
|
|
80
80
|
*
|
|
81
|
+
* 0.7.12 (2026-11-05): the project moved to Neywa-Labs/mlx-vlm; same PyPI
|
|
82
|
+
* name. It resolves mlx 0.32.4 (mlx-audio pins it exactly) and transformers
|
|
83
|
+
* 5.22. Every private `speculative.mtp` helper the assisted round loop calls
|
|
84
|
+
* is unchanged for Qwen 3.5; Gemma 4 targets now rebuild the drafter's shared
|
|
85
|
+
* KV from committed caches, which `shared_kv_after_round` follows. Its
|
|
86
|
+
* qwen3_coder parser keeps "null" as text in a string parameter, which
|
|
87
|
+
* `tool_args_json.py` now mirrors. Verified on an M5 Max against 0.7.1
|
|
88
|
+
* through the real sidecar: gemma4-e4b and qwen3.8-27b + MTP drafter render
|
|
89
|
+
* identical prompt token counts and identical tool-call, JSON and vision
|
|
90
|
+
* output; 0.7.1-written disk caches load and extend (Gemma 4 rotating,
|
|
91
|
+
* Qwen 3.5 hybrid). Greedy MTP matches greedy no-spec on code at 0-30k
|
|
92
|
+
* tokens; on prose both versions flip bf16 one-step ties (top-2 gap 0.125
|
|
93
|
+
* nats), which two non-speculative decode paths also disagree on, so prose
|
|
94
|
+
* is not an exactness probe. Clean A/B/B/A decode on qwen3.8-27b-q4: MTP on
|
|
95
|
+
* the vlm tower at parity to 10k and +19% at 50k; vlm tower without MTP 2.0x
|
|
96
|
+
* at 50k (7.3 -> 14.8 tok/s), now level with the mlx_lm tower; mlx_lm tower
|
|
97
|
+
* 0.94x at 0-10k, within run-to-run noise.
|
|
98
|
+
*
|
|
81
99
|
* Keep upgrades explicit and re-sweep the MLX catalog whenever this pin
|
|
82
100
|
* moves. */
|
|
83
|
-
export const MLX_DEFAULT_PACKAGE_SPEC = 'mlx-vlm==0.7.
|
|
101
|
+
export const MLX_DEFAULT_PACKAGE_SPEC = 'mlx-vlm==0.7.12';
|
|
84
102
|
/** Venv name passed to `UvRuntime.ensureVenv` (and its dir basename). */
|
|
85
103
|
export const MLX_VENV_NAME = 'mlx';
|
|
86
104
|
/**
|
|
@@ -100,7 +118,8 @@ export function mlxVenvPackages(packageSpec) {
|
|
|
100
118
|
// a dependency (0.6.6 did, which is the only reason the old list
|
|
101
119
|
// worked). Dropping this pin kills the batch engine at import time.
|
|
102
120
|
// 0.31.3 is also the combination used for the 0.7.1 Qwen4-Exp batch
|
|
103
|
-
// compatibility check above; keep it
|
|
121
|
+
// compatibility check and the 0.7.12 verification above; keep it
|
|
122
|
+
// independent of mlx-vlm's deps.
|
|
104
123
|
return [
|
|
105
124
|
packageSpec ?? MLX_DEFAULT_PACKAGE_SPEC,
|
|
106
125
|
'mlx-lm==0.31.3',
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { NativeCapacityCommand, NativeCapacityReply } from '@bendyline/gezel';
|
|
2
|
-
import { readProcessIdentities } from './process-identity.js';
|
|
2
|
+
import { ProcessIdentityCache, processAlive, type readProcessIdentities } from './process-identity.js';
|
|
3
|
+
export { processAlive };
|
|
3
4
|
export interface DeviceCapacitySample {
|
|
4
5
|
budgetBytes: number;
|
|
5
6
|
gpuBudgetBytes: number;
|
|
@@ -14,7 +15,6 @@ export interface DeviceCapacitySample {
|
|
|
14
15
|
availableRamBytes?: number;
|
|
15
16
|
serializeLoads: boolean;
|
|
16
17
|
}
|
|
17
|
-
export declare function processAlive(pid: number): boolean;
|
|
18
18
|
/**
|
|
19
19
|
* SQLite's process lock makes check-and-reserve atomic across GEZEL_HOME values.
|
|
20
20
|
* Reservations survive a broker restart. A dead parent does not free the memory
|
|
@@ -22,11 +22,13 @@ export declare function processAlive(pid: number): boolean;
|
|
|
22
22
|
*/
|
|
23
23
|
export declare class DeviceCapacityLedger {
|
|
24
24
|
private readonly options;
|
|
25
|
+
private readonly identityCache;
|
|
25
26
|
constructor(options: {
|
|
26
27
|
directory: string;
|
|
27
28
|
sample: () => Promise<DeviceCapacitySample>;
|
|
28
29
|
alive?: (pid: number) => boolean;
|
|
29
30
|
identities?: typeof readProcessIdentities;
|
|
31
|
+
identityCache?: ProcessIdentityCache;
|
|
30
32
|
});
|
|
31
33
|
execute(command: NativeCapacityCommand): Promise<NativeCapacityReply>;
|
|
32
34
|
private transact;
|
|
@@ -2,7 +2,8 @@ import { chmod, mkdir } from 'node:fs/promises';
|
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { CapacityDeniedError } from './capacity-broker.js';
|
|
5
|
-
import {
|
|
5
|
+
import { ProcessIdentityCache, processAlive, sharedProcessIdentities, } from './process-identity.js';
|
|
6
|
+
export { processAlive };
|
|
6
7
|
// Keep the newer builtin behind a runtime require. The current tsup/esbuild
|
|
7
8
|
// target rewrites a static `node:sqlite` import to the nonexistent npm package
|
|
8
9
|
// `sqlite`, making the daemon bundle fail before it can write runtime state.
|
|
@@ -11,15 +12,6 @@ const { DatabaseSync } = nodeRequire('node:sqlite');
|
|
|
11
12
|
function gb(bytes) {
|
|
12
13
|
return `${(bytes / 1024 ** 3).toFixed(1)} GB`;
|
|
13
14
|
}
|
|
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
|
-
}
|
|
23
15
|
/**
|
|
24
16
|
* SQLite's process lock makes check-and-reserve atomic across GEZEL_HOME values.
|
|
25
17
|
* Reservations survive a broker restart. A dead parent does not free the memory
|
|
@@ -27,8 +19,18 @@ export function processAlive(pid) {
|
|
|
27
19
|
*/
|
|
28
20
|
export class DeviceCapacityLedger {
|
|
29
21
|
options;
|
|
22
|
+
identityCache;
|
|
30
23
|
constructor(options) {
|
|
31
24
|
this.options = options;
|
|
25
|
+
// An injected reader still goes through a cache, so tests see the reads
|
|
26
|
+
// production makes. An injected `alive` alone checks liveness only.
|
|
27
|
+
this.identityCache =
|
|
28
|
+
options.identityCache ??
|
|
29
|
+
(options.identities
|
|
30
|
+
? new ProcessIdentityCache({ read: options.identities, alive: options.alive })
|
|
31
|
+
: options.alive
|
|
32
|
+
? undefined
|
|
33
|
+
: sharedProcessIdentities);
|
|
32
34
|
}
|
|
33
35
|
async execute(command) {
|
|
34
36
|
const sample = await this.options.sample();
|
|
@@ -43,18 +45,31 @@ export class DeviceCapacityLedger {
|
|
|
43
45
|
.prepare('SELECT claim FROM claims')
|
|
44
46
|
.all()
|
|
45
47
|
.map((row) => JSON.parse(String(row.claim)));
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
]);
|
|
50
|
-
if (command.action === 'acquire')
|
|
51
|
-
pids.push(command.ownerPid);
|
|
48
|
+
const record = [];
|
|
49
|
+
if (command.action === 'acquire' && !snapshot.some((claim) => claim.id === command.id))
|
|
50
|
+
record.push(command.ownerPid);
|
|
52
51
|
if (command.action === 'bind')
|
|
53
|
-
|
|
52
|
+
record.push(command.childPid);
|
|
53
|
+
// Stamps only decide which reservations are stale, and a stale one costs
|
|
54
|
+
// someone only while a request waits for memory. With no request
|
|
55
|
+
// waiting, liveness alone prunes, so the status check that runs before
|
|
56
|
+
// every engine request reads no stamps.
|
|
57
|
+
const confirm = new Map();
|
|
58
|
+
const stamp = (pid, identity) => {
|
|
59
|
+
if (pid !== undefined && identity !== undefined)
|
|
60
|
+
confirm.set(pid, [...(confirm.get(pid) ?? []), identity]);
|
|
61
|
+
};
|
|
62
|
+
if (command.action === 'acquire' || snapshot.some((claim) => claim.phase === 'waiting')) {
|
|
63
|
+
for (const claim of snapshot) {
|
|
64
|
+
stamp(claim.childPid, claim.childIdentity);
|
|
65
|
+
// An owner's stamp is only consulted while its claim is queued.
|
|
66
|
+
if (claim.phase === 'waiting')
|
|
67
|
+
stamp(claim.ownerPid, claim.ownerIdentity);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
54
70
|
// OS probes may spawn a helper. Never hold SQLite's writer lock while awaiting one.
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
? await probe(pids).catch(() => new Map())
|
|
71
|
+
const identities = command.action !== 'release' && this.identityCache
|
|
72
|
+
? await this.identityCache.lookup(record, confirm).catch(() => new Map())
|
|
58
73
|
: new Map();
|
|
59
74
|
db.exec('BEGIN IMMEDIATE');
|
|
60
75
|
try {
|
|
@@ -77,6 +77,10 @@ export declare function staticCapacityCeilings(budget: Pick<CapacityBudget, 'bud
|
|
|
77
77
|
budgetBytes: number;
|
|
78
78
|
gpuBudgetBytes: number;
|
|
79
79
|
};
|
|
80
|
+
/** The RAM ceiling the device ledger admits one launch under, for sizing a launch to fit it. */
|
|
81
|
+
export declare function deviceModelBudgetBytes(config: {
|
|
82
|
+
localEngineMemoryGb?: number | null | undefined;
|
|
83
|
+
}): Promise<number>;
|
|
80
84
|
export declare function sampleDeviceCapacity(home?: string): Promise<DeviceCapacitySample>;
|
|
81
85
|
export declare function localDeviceCapacity(home: string): DeviceCapacityLedger;
|
|
82
86
|
/** Conservative fallback for media engines that do not expose a launch planner. */
|
|
@@ -155,6 +155,11 @@ export function staticCapacityCeilings(budget, ramBytes, configuredBytes, platfo
|
|
|
155
155
|
: budget.fastBytes),
|
|
156
156
|
};
|
|
157
157
|
}
|
|
158
|
+
/** The RAM ceiling the device ledger admits one launch under, for sizing a launch to fit it. */
|
|
159
|
+
export async function deviceModelBudgetBytes(config) {
|
|
160
|
+
const ram = totalMemoryBytes();
|
|
161
|
+
return staticCapacityCeilings(await measuredCapacityBudget(), ram, configuredEngineMemoryBytes(config)).budgetBytes;
|
|
162
|
+
}
|
|
158
163
|
export async function sampleDeviceCapacity(home) {
|
|
159
164
|
const budget = await measuredCapacityBudget();
|
|
160
165
|
const ram = totalMemoryBytes();
|
|
@@ -37,7 +37,7 @@ export declare class LocalEngineRuntime {
|
|
|
37
37
|
/** Product chat may prefill its own sessions; the engine runtime never resolves them. */
|
|
38
38
|
protected prefillCacheSession(_sessionId: string): Promise<boolean>;
|
|
39
39
|
/** Cached provider instances keyed by provider name. */
|
|
40
|
-
protected readonly providers: Map<"copilot" | "openai" | "anthropic" | "anthropic-cli" | "codex-cli" | "ollama" | "llama-cpp" | "mlx" | "ds4" | "apple-foundation-models" | "android-mlkit"
|
|
40
|
+
protected readonly providers: Map<"copilot" | "openai" | "anthropic" | "anthropic-cli" | "codex-cli" | "ollama" | "llama-cpp" | "mlx" | "ds4" | "remote" | "apple-foundation-models" | "android-mlkit", LLMProvider>;
|
|
41
41
|
protected readonly catalog: CatalogService;
|
|
42
42
|
protected readonly llamaCppModels?: import('../llama-cpp/index.js').LlamaCppModelManager;
|
|
43
43
|
protected readonly ds4Models?: import('../llama-cpp/index.js').LlamaCppModelManager;
|
|
@@ -384,6 +384,21 @@ export declare class LocalEngineRuntime {
|
|
|
384
384
|
* the only guard against a window the engine cannot actually serve.
|
|
385
385
|
*/
|
|
386
386
|
contextCeilingTokens?: number;
|
|
387
|
+
/** ds4 only: whether the launch would stream routed experts from SSD. */
|
|
388
|
+
ssdStreaming?: boolean;
|
|
389
|
+
/** ds4 only: `plannedResidentBytes` comes from the entry's measurement for this mode. */
|
|
390
|
+
memoryMeasured?: boolean;
|
|
391
|
+
/**
|
|
392
|
+
* ds4 only, streamed launches with a measured footprint: the expert cache
|
|
393
|
+
* the launcher would choose, so the model list's fit verdict comes from the
|
|
394
|
+
* same plan instead of a copy of its headroom rule.
|
|
395
|
+
*/
|
|
396
|
+
expertCache?: {
|
|
397
|
+
cacheBytes: number;
|
|
398
|
+
requestedBytes: number;
|
|
399
|
+
clamped: boolean;
|
|
400
|
+
safe: boolean;
|
|
401
|
+
};
|
|
387
402
|
}>;
|
|
388
403
|
/**
|
|
389
404
|
* Cancel a pending provider-queue entry by (provider, id). Resolves
|