@bendyline/gezel-inference 1.0.8 → 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/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 +3 -2
- 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/python/spec_decode.py +23 -2
- package/dist/providers/mlx/python/tool_args_json.py +59 -21
- package/dist/providers/mlx/venv.d.ts +19 -1
- package/dist/providers/mlx/venv.js +21 -2
- 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/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/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
|
|
@@ -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',
|
|
@@ -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
|
|
@@ -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
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
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
|
-
|
|
612
|
-
|
|
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
|
-
...(
|
|
623
|
-
? { 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
|
-
})
|
|
628
|
-
|
|
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
|
-
|
|
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
|
-
...(
|
|
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
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
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
|
-
|
|
1154
|
+
ssdStreaming,
|
|
1155
|
+
...(measured
|
|
1113
1156
|
? {
|
|
1114
|
-
|
|
1157
|
+
memoryMeasured: true,
|
|
1158
|
+
weightsResidentBytes: measured.contextFreeBytes,
|
|
1115
1159
|
kvFixedBytesPerSlot: 0,
|
|
1116
|
-
kvBytesPerTokenPerSlot:
|
|
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
|
|
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 (
|
|
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 {
|
|
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
|
-
|
|
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 {
|
|
@@ -516,7 +516,14 @@ export declare class NativeEngineSupervisor {
|
|
|
516
516
|
private clearFreezeTimer;
|
|
517
517
|
private clearHealthTimer;
|
|
518
518
|
}
|
|
519
|
-
/**
|
|
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
|
-
/**
|
|
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;
|
package/dist/remotes/types.d.ts
CHANGED
|
@@ -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
|
|
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;
|