@camstack/addon-ai 0.4.3 → 0.4.5
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/addon.js +1477 -436
- package/dist/addon.mjs +1480 -439
- package/dist/{ensure-llama-server-v65evjK2.mjs → ensure-llama-server-COC6iveo.mjs} +0 -0
- package/dist/index.mjs +2 -2
- package/package.json +1 -1
package/dist/addon.mjs
CHANGED
|
@@ -2,9 +2,9 @@ import { createRequire } from "node:module";
|
|
|
2
2
|
import * as path$1 from "node:path";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
4
|
import { connect, createServer } from "node:net";
|
|
5
|
-
import { promisify } from "node:util";
|
|
6
5
|
import * as fs from "node:fs";
|
|
7
|
-
import { createReadStream } from "node:fs";
|
|
6
|
+
import { createReadStream, existsSync, statSync } from "node:fs";
|
|
7
|
+
import { promisify } from "node:util";
|
|
8
8
|
import { brotliCompress, gzip } from "node:zlib";
|
|
9
9
|
import * as fsp from "node:fs/promises";
|
|
10
10
|
import { spawn } from "node:child_process";
|
|
@@ -11047,6 +11047,8 @@ var QueryFilterSchema = object({
|
|
|
11047
11047
|
where: record(string(), unknown()).optional(),
|
|
11048
11048
|
whereIn: record(string(), array(unknown())).optional(),
|
|
11049
11049
|
whereBetween: record(string(), tuple([unknown(), unknown()])).optional(),
|
|
11050
|
+
/** NULL-safe exclusion: matches rows whose field is NULL OR != the value. */
|
|
11051
|
+
whereNot: record(string(), unknown()).optional(),
|
|
11050
11052
|
orderBy: object({
|
|
11051
11053
|
field: string(),
|
|
11052
11054
|
direction: _enum(["asc", "desc"])
|
|
@@ -11066,7 +11068,8 @@ var QueryFilterSchema = object({
|
|
|
11066
11068
|
var MutationFilterSchema = object({
|
|
11067
11069
|
where: record(string(), unknown()).optional(),
|
|
11068
11070
|
whereIn: record(string(), array(unknown())).optional(),
|
|
11069
|
-
whereBetween: record(string(), tuple([unknown(), unknown()])).optional()
|
|
11071
|
+
whereBetween: record(string(), tuple([unknown(), unknown()])).optional(),
|
|
11072
|
+
whereNot: record(string(), unknown()).optional()
|
|
11070
11073
|
});
|
|
11071
11074
|
/** A single stored record: `{ id, data }`. */
|
|
11072
11075
|
var SettingsRecordSchema = object({
|
|
@@ -12500,6 +12503,18 @@ var LlmGenerateBaseInputSchema = object({
|
|
|
12500
12503
|
* Resource ceiling = llama-server flags + idleStopMinutes ONLY (no RSS
|
|
12501
12504
|
* watchdog — operator decision #3).
|
|
12502
12505
|
*/
|
|
12506
|
+
/**
|
|
12507
|
+
* A companion artifact that MUST land beside the main GGUF: the `mmproj`
|
|
12508
|
+
* projector of a vision model, or shards 2..N of a split GGUF. Carried on the
|
|
12509
|
+
* REF rather than looked up at install time, so what the operator approved in
|
|
12510
|
+
* the preview is exactly what the node downloads.
|
|
12511
|
+
*/
|
|
12512
|
+
var ManagedModelExtraFileSchema = object({
|
|
12513
|
+
url: string(),
|
|
12514
|
+
filename: string(),
|
|
12515
|
+
sizeBytes: number$1(),
|
|
12516
|
+
sha256: string().optional()
|
|
12517
|
+
});
|
|
12503
12518
|
var ManagedModelRefSchema = discriminatedUnion("kind", [
|
|
12504
12519
|
object({
|
|
12505
12520
|
kind: literal("catalog"),
|
|
@@ -12508,7 +12523,11 @@ var ManagedModelRefSchema = discriminatedUnion("kind", [
|
|
|
12508
12523
|
object({
|
|
12509
12524
|
kind: literal("url"),
|
|
12510
12525
|
url: string(),
|
|
12511
|
-
sha256: string().optional()
|
|
12526
|
+
sha256: string().optional(),
|
|
12527
|
+
/** Picker/status label; the file basename when absent. */
|
|
12528
|
+
label: string().optional(),
|
|
12529
|
+
sizeBytes: number$1().optional(),
|
|
12530
|
+
extraFiles: array(ManagedModelExtraFileSchema).optional()
|
|
12512
12531
|
}),
|
|
12513
12532
|
object({
|
|
12514
12533
|
kind: literal("path"),
|
|
@@ -12569,11 +12588,39 @@ var ManagedRuntimeConfigSchema = object({
|
|
|
12569
12588
|
"q4_1",
|
|
12570
12589
|
"q4_0"
|
|
12571
12590
|
]).optional(),
|
|
12591
|
+
/**
|
|
12592
|
+
* Escape hatch for llama-server flags this schema does NOT model — `--jinja`
|
|
12593
|
+
* (which most vision chat templates need and some language-only models
|
|
12594
|
+
* dislike), `--cont-batching`, `--rope-scaling`, …
|
|
12595
|
+
*
|
|
12596
|
+
* It is NOT a second place to set the flags above. A token that collides
|
|
12597
|
+
* with a typed field is REJECTED at start, naming the field that owns it
|
|
12598
|
+
* (`assertNoOwnedFlags`), because two knobs writing the same argv is exactly
|
|
12599
|
+
* the "two switches that disagree" failure this repo has already shipped
|
|
12600
|
+
* twice (D62).
|
|
12601
|
+
*/
|
|
12602
|
+
extraArgs: array(string()).default([]),
|
|
12572
12603
|
/** Else lazy: first generate boots it. */
|
|
12573
12604
|
autoStart: boolean().default(false),
|
|
12574
12605
|
/** 0 = never; frees RAM after quiet periods. */
|
|
12575
12606
|
idleStopMinutes: number$1().int().default(30)
|
|
12576
12607
|
});
|
|
12608
|
+
/**
|
|
12609
|
+
* Where a multi-GB install currently is. A single 0..1 fraction cannot answer
|
|
12610
|
+
* "is it stuck?" for an install that is three files (shards + mmproj) followed
|
|
12611
|
+
* by a sha256 pass over 22 GB — during which the fraction sat at 1.0 and the
|
|
12612
|
+
* node looked hung. Phase + file + bytes is the smallest shape that does.
|
|
12613
|
+
*/
|
|
12614
|
+
var LlmDownloadProgressSchema = object({
|
|
12615
|
+
phase: _enum(["downloading", "verifying"]),
|
|
12616
|
+
/** The artifact currently moving, e.g. `mmproj-F16.gguf`. */
|
|
12617
|
+
file: string(),
|
|
12618
|
+
fileIndex: number$1().int(),
|
|
12619
|
+
fileCount: number$1().int(),
|
|
12620
|
+
/** Across the WHOLE install, not the current file. */
|
|
12621
|
+
downloadedBytes: number$1(),
|
|
12622
|
+
totalBytes: number$1().optional()
|
|
12623
|
+
});
|
|
12577
12624
|
var LlmRuntimeStatusSchema = object({
|
|
12578
12625
|
/** Status is ALWAYS node-qualified. */
|
|
12579
12626
|
nodeId: string(),
|
|
@@ -12590,6 +12637,8 @@ var LlmRuntimeStatusSchema = object({
|
|
|
12590
12637
|
modelPath: string().optional(),
|
|
12591
12638
|
modelId: string().optional(),
|
|
12592
12639
|
downloadProgress: number$1().min(0).max(1).optional(),
|
|
12640
|
+
/** Detail behind `downloadProgress`; present for the same lifetime. */
|
|
12641
|
+
download: LlmDownloadProgressSchema.optional(),
|
|
12593
12642
|
lastError: string().optional(),
|
|
12594
12643
|
crashesInWindow: number$1(),
|
|
12595
12644
|
/** Child RSS (sampled best-effort). */
|
|
@@ -12600,7 +12649,14 @@ var LlmNodeModelSchema = object({
|
|
|
12600
12649
|
file: string(),
|
|
12601
12650
|
sizeBytes: number$1(),
|
|
12602
12651
|
catalogId: string().optional(),
|
|
12603
|
-
installedAt: number$1().optional()
|
|
12652
|
+
installedAt: number$1().optional(),
|
|
12653
|
+
/**
|
|
12654
|
+
* Absolute path on the node. Present so a file that is on disk but matches
|
|
12655
|
+
* no catalog entry — a custom Hugging Face install, or a GGUF the operator
|
|
12656
|
+
* copied in by hand — is still SELECTABLE, as a `{kind:'path'}` ref. Without
|
|
12657
|
+
* it the picker could list such a file and do nothing with it.
|
|
12658
|
+
*/
|
|
12659
|
+
path: string().optional()
|
|
12604
12660
|
});
|
|
12605
12661
|
var LlmRuntimeDiskUsageSchema = object({
|
|
12606
12662
|
nodeId: string(),
|
|
@@ -12761,6 +12817,36 @@ var ManagedModelCatalogEntrySchema = object({
|
|
|
12761
12817
|
/** Vision models: companion projector file. */
|
|
12762
12818
|
mmprojUrl: string().optional()
|
|
12763
12819
|
});
|
|
12820
|
+
/**
|
|
12821
|
+
* The outcome of turning one operator-typed Hugging Face reference into a
|
|
12822
|
+
* download plan. A RESULT, never a throw: "this repo has 24 quantizations and
|
|
12823
|
+
* I will not pick for you" is a normal answer the UI has to render, not an
|
|
12824
|
+
* exception.
|
|
12825
|
+
*
|
|
12826
|
+
* `candidates` is the whole reason the refusal is usable — every string in it
|
|
12827
|
+
* is a tag that resolves when pasted back as `<org>/<repo>:<TAG>`.
|
|
12828
|
+
*/
|
|
12829
|
+
var HfModelResolutionSchema = discriminatedUnion("ok", [object({
|
|
12830
|
+
ok: literal(true),
|
|
12831
|
+
/** Ready to hand to `installModel` unchanged. */
|
|
12832
|
+
model: ManagedModelRefSchema,
|
|
12833
|
+
label: string(),
|
|
12834
|
+
repo: string(),
|
|
12835
|
+
quantization: string(),
|
|
12836
|
+
purpose: _enum(["text", "vision"]),
|
|
12837
|
+
totalBytes: number$1(),
|
|
12838
|
+
/** mmproj + shards, for the preview: an operator approving 23 GB should
|
|
12839
|
+
* see that 0.9 GB of it is a projector they did not name. */
|
|
12840
|
+
extraFilenames: array(string())
|
|
12841
|
+
}), object({
|
|
12842
|
+
ok: literal(false),
|
|
12843
|
+
code: string(),
|
|
12844
|
+
message: string(),
|
|
12845
|
+
candidates: array(string()).optional(),
|
|
12846
|
+
/** Set when the refusal was only the ceiling: re-calling with
|
|
12847
|
+
* `maxBytes: requiredBytes` is the operator's explicit override. */
|
|
12848
|
+
requiredBytes: number$1().optional()
|
|
12849
|
+
})]);
|
|
12764
12850
|
var LlmRuntimeNodeSchema = object({
|
|
12765
12851
|
nodeId: string(),
|
|
12766
12852
|
reachable: boolean(),
|
|
@@ -12828,6 +12914,25 @@ var llmCapability = {
|
|
|
12828
12914
|
listModelCatalog: method(object({}), array(ManagedModelCatalogEntrySchema)),
|
|
12829
12915
|
listRuntimeNodes: method(object({}), array(LlmRuntimeNodeSchema)),
|
|
12830
12916
|
listNodeModels: method(object({ nodeId: string() }), array(LlmNodeModelSchema)),
|
|
12917
|
+
/**
|
|
12918
|
+
* One typed Hugging Face reference → a pinned, verified `ManagedModelRef`.
|
|
12919
|
+
*
|
|
12920
|
+
* Runs on the HUB, not on the target node: resolution needs egress to
|
|
12921
|
+
* huggingface.co, and an agent that cannot reach it still installs fine
|
|
12922
|
+
* through the model-distributor relay. Nothing is downloaded here — this is
|
|
12923
|
+
* a tree read plus a HEAD, so the operator sees the size, the quantization
|
|
12924
|
+
* and the mmproj BEFORE approving a multi-GB pull.
|
|
12925
|
+
*/
|
|
12926
|
+
resolveModelRef: method(object({
|
|
12927
|
+
/** `https://huggingface.co/<org>/<repo>/resolve/main/<f>.gguf`,
|
|
12928
|
+
* `<org>/<repo>/<f>.gguf`, `<org>/<repo>` or `<org>/<repo>:<QUANT>`. */
|
|
12929
|
+
ref: string(),
|
|
12930
|
+
/** Explicit ceiling override, in bytes. Absent = the built-in ceiling. */
|
|
12931
|
+
maxBytes: number$1().positive().optional()
|
|
12932
|
+
}), HfModelResolutionSchema, {
|
|
12933
|
+
kind: "mutation",
|
|
12934
|
+
auth: "admin"
|
|
12935
|
+
}),
|
|
12831
12936
|
installModel: method(object({
|
|
12832
12937
|
nodeId: string(),
|
|
12833
12938
|
model: ManagedModelRefSchema
|
|
@@ -16921,7 +17026,10 @@ var RecentTracksQueryInput = object({
|
|
|
16921
17026
|
* Encodes the (lastSeen, trackId) sort position — treat as opaque. */
|
|
16922
17027
|
cursor: string().optional(),
|
|
16923
17028
|
/** See {@link TrackProjectionSchema}. Default `full`. */
|
|
16924
|
-
projection: TrackProjectionSchema.optional()
|
|
17029
|
+
projection: TrackProjectionSchema.optional(),
|
|
17030
|
+
/** Include stationary-promoted rows (parked objects). Default false: the
|
|
17031
|
+
* feed lists passages; parking records live on the stationary registry. */
|
|
17032
|
+
includeStationary: boolean().optional()
|
|
16925
17033
|
});
|
|
16926
17034
|
var RecentTracksPageSchema = object({
|
|
16927
17035
|
/** Merged page, ordered by (`lastSeen` DESC, `trackId` DESC). */
|
|
@@ -17139,7 +17247,11 @@ DeviceType.Camera, method(object({ deviceId: number$1() }), array(TrackSchema).r
|
|
|
17139
17247
|
zone: TrackZoneFilterSchema.optional(),
|
|
17140
17248
|
/** See {@link TrackProjectionSchema}. Default `full` (backward
|
|
17141
17249
|
* compatible — omitting the field keeps today's exact behaviour). */
|
|
17142
|
-
projection: TrackProjectionSchema.optional()
|
|
17250
|
+
projection: TrackProjectionSchema.optional(),
|
|
17251
|
+
/** Include stationary-promoted rows (parked objects handed to the
|
|
17252
|
+
* stationary registry). Default false: the timeline lists passages,
|
|
17253
|
+
* not parking records (operator decision, 2026-08-15). */
|
|
17254
|
+
includeStationary: boolean().optional()
|
|
17143
17255
|
}), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(object({ deviceId: number$1() }), _void(), {
|
|
17144
17256
|
kind: "mutation",
|
|
17145
17257
|
auth: "admin"
|
|
@@ -17303,11 +17415,16 @@ DeviceType.Camera, method(object({ deviceId: number$1() }), array(TrackSchema).r
|
|
|
17303
17415
|
auth: "admin"
|
|
17304
17416
|
}), method(object({
|
|
17305
17417
|
eventId: string(),
|
|
17306
|
-
kind: MediaFileKindEnum.optional()
|
|
17418
|
+
kind: MediaFileKindEnum.optional(),
|
|
17419
|
+
deviceId: number$1()
|
|
17420
|
+
}), array(MediaFileSchema).readonly()), method(object({
|
|
17421
|
+
trackId: string(),
|
|
17422
|
+
kinds: array(MediaFileKindEnum).optional(),
|
|
17423
|
+
deviceId: number$1()
|
|
17307
17424
|
}), array(MediaFileSchema).readonly()), method(object({
|
|
17308
17425
|
trackId: string(),
|
|
17309
|
-
|
|
17310
|
-
}), array(
|
|
17426
|
+
deviceId: number$1()
|
|
17427
|
+
}), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
|
|
17311
17428
|
kind: "mutation",
|
|
17312
17429
|
auth: "admin"
|
|
17313
17430
|
}), method(RebuildObjectEmbeddingsInput, RebuildObjectEmbeddingsResultSchema, {
|
|
@@ -17967,6 +18084,17 @@ var maxSessionHoldMsField = {
|
|
|
17967
18084
|
default: 12e4,
|
|
17968
18085
|
step: 5e3
|
|
17969
18086
|
};
|
|
18087
|
+
/**
|
|
18088
|
+
* Quiet period that closes an `audioMode: 'on-motion'` audio window. Floor of
|
|
18089
|
+
* 5s so a rearm can never degenerate into per-event stream churn; default 90s
|
|
18090
|
+
* comfortably outlives the gap between two PIR wakes on a battery camera.
|
|
18091
|
+
*/
|
|
18092
|
+
var audioMotionWindowMsField = {
|
|
18093
|
+
min: 5e3,
|
|
18094
|
+
max: 6e5,
|
|
18095
|
+
default: 9e4,
|
|
18096
|
+
step: 5e3
|
|
18097
|
+
};
|
|
17970
18098
|
var motionFpsField = {
|
|
17971
18099
|
min: 1,
|
|
17972
18100
|
max: 30,
|
|
@@ -18143,6 +18271,27 @@ var RunnerCameraConfigSchema = object({
|
|
|
18143
18271
|
* resolved `CameraDetectionConfig`.
|
|
18144
18272
|
*/
|
|
18145
18273
|
maxSessionHoldMs: number$1().min(maxSessionHoldMsField.min).max(maxSessionHoldMsField.max).optional(),
|
|
18274
|
+
/**
|
|
18275
|
+
* Orchestrator-side quiet period (ms) that closes an `audioMode:
|
|
18276
|
+
* 'on-motion'` audio window, measured from the LAST motion event.
|
|
18277
|
+
*
|
|
18278
|
+
* This exists because the falling edge cannot be relied on. Camera-native
|
|
18279
|
+
* providers emit motion as a RISING EDGE ONLY (Reolink's Baichuan push and
|
|
18280
|
+
* its email-push SMTP path both emit `detected: true` and never the
|
|
18281
|
+
* counterpart); only the frame-diff analyzer emits falls. So on an
|
|
18282
|
+
* onboard-only camera a window that closed only on `detected: false` never
|
|
18283
|
+
* closed at all, and `on-motion` silently behaved as `always-on` — on a
|
|
18284
|
+
* battery camera, the one failure mode the mode exists to prevent.
|
|
18285
|
+
*
|
|
18286
|
+
* Every motion event rearms this timer WITHOUT restarting the stream, so a
|
|
18287
|
+
* burst of re-fires costs nothing. A falling edge, when one does arrive,
|
|
18288
|
+
* still closes earlier via `motionCooldownMs` — whichever comes first wins.
|
|
18289
|
+
*
|
|
18290
|
+
* Not consumed by the runner: carried here so it shares the per-camera
|
|
18291
|
+
* device-settings surface with `motionCooldownMs`, exactly like
|
|
18292
|
+
* `maxSessionHoldMs`.
|
|
18293
|
+
*/
|
|
18294
|
+
audioMotionWindowMs: number$1().min(audioMotionWindowMsField.min).max(audioMotionWindowMsField.max).optional(),
|
|
18146
18295
|
motionFps: number$1().min(motionFpsField.min).max(motionFpsField.max).default(motionFpsField.default),
|
|
18147
18296
|
detectionFps: number$1().min(detectionFpsField.min).max(detectionFpsField.max).default(detectionFpsField.default),
|
|
18148
18297
|
motionStreamId: string(),
|
|
@@ -18238,7 +18387,7 @@ var RunnerCameraConfigSchema = object({
|
|
|
18238
18387
|
*/
|
|
18239
18388
|
inferenceDevices: array(RunnerInferenceDeviceSchema).readonly().optional()
|
|
18240
18389
|
});
|
|
18241
|
-
motionFpsField.min, motionFpsField.max, motionFpsField.step, motionFpsField.default, detectionFpsField.min, detectionFpsField.max, detectionFpsField.step, detectionFpsField.default, motionCooldownMsField.min, motionCooldownMsField.max, motionCooldownMsField.step, motionCooldownMsField.default, maxSessionHoldMsField.min, maxSessionHoldMsField.max, maxSessionHoldMsField.step, maxSessionHoldMsField.default, occupancyRecheckSecField.min, occupancyRecheckSecField.max, occupancyRecheckSecField.step, occupancyRecheckSecField.default, occupancyRecheckFramesField.min, occupancyRecheckFramesField.max, occupancyRecheckFramesField.step, occupancyRecheckFramesField.default;
|
|
18390
|
+
motionFpsField.min, motionFpsField.max, motionFpsField.step, motionFpsField.default, detectionFpsField.min, detectionFpsField.max, detectionFpsField.step, detectionFpsField.default, motionCooldownMsField.min, motionCooldownMsField.max, motionCooldownMsField.step, motionCooldownMsField.default, maxSessionHoldMsField.min, maxSessionHoldMsField.max, maxSessionHoldMsField.step, maxSessionHoldMsField.default, audioMotionWindowMsField.min, audioMotionWindowMsField.max, audioMotionWindowMsField.step, audioMotionWindowMsField.default, occupancyRecheckSecField.min, occupancyRecheckSecField.max, occupancyRecheckSecField.step, occupancyRecheckSecField.default, occupancyRecheckFramesField.min, occupancyRecheckFramesField.max, occupancyRecheckFramesField.step, occupancyRecheckFramesField.default;
|
|
18242
18391
|
/**
|
|
18243
18392
|
* Runtime load summary returned by `getLocalLoad`. Used by the orchestrator's
|
|
18244
18393
|
* load-balancing levels (L2 capacity-based, L3 hardware-aware) to decide
|
|
@@ -19254,7 +19403,16 @@ targets: array(object({
|
|
|
19254
19403
|
/** A sleeping battery camera: the frame is deliberately stale and will
|
|
19255
19404
|
* NOT refresh in the background. A surface should say so rather than
|
|
19256
19405
|
* present it as current. */
|
|
19257
|
-
sleeping: boolean()
|
|
19406
|
+
sleeping: boolean(),
|
|
19407
|
+
/** Current device state rendered over the cached frame. State images
|
|
19408
|
+
* remain authoritative even when their photographic background is
|
|
19409
|
+
* old; null means the link must carry a current camera frame. */
|
|
19410
|
+
stateReason: _enum([
|
|
19411
|
+
"disabled",
|
|
19412
|
+
"sleeping",
|
|
19413
|
+
"unreachable",
|
|
19414
|
+
"waking"
|
|
19415
|
+
]).nullable()
|
|
19258
19416
|
})));
|
|
19259
19417
|
/**
|
|
19260
19418
|
* `sso-bridge` — internal hub-only cap that lets SSO-style auth
|
|
@@ -20776,6 +20934,25 @@ var BatteryStatusSchema = object({
|
|
|
20776
20934
|
/** Ms epoch of the last observation. Lets consumers reason about freshness. */
|
|
20777
20935
|
lastUpdated: number$1(),
|
|
20778
20936
|
/**
|
|
20937
|
+
* Ms epoch of the last time the device PROVED it was reachable — a
|
|
20938
|
+
* completed firmware round-trip, an observed wake, or an inbound push
|
|
20939
|
+
* (firmware event, email). `0`/absent = never since this slice was born.
|
|
20940
|
+
*
|
|
20941
|
+
* This is the ONLY input that separates "asleep" from "gone", and it is
|
|
20942
|
+
* fed exclusively by PASSIVE signals: nothing may write it by reaching
|
|
20943
|
+
* for the radio, because a poll that confirms reachability is the same
|
|
20944
|
+
* poll that drains the battery. See {@link deriveBatteryPresence} — the
|
|
20945
|
+
* single derivation every consumer must use; no surface computes its own.
|
|
20946
|
+
*
|
|
20947
|
+
* It is deliberately NOT a clock in the
|
|
20948
|
+
* `scripts/check-runtime-state-durability.ts` sense: it is the
|
|
20949
|
+
* observation itself, and it is the only thing a 30-hour silence is
|
|
20950
|
+
* visible in. Writers quantise it (see `CONTACT_WRITE_QUANTUM_MS` in the
|
|
20951
|
+
* Reolink provider) so a value that means "recently" cannot cost a
|
|
20952
|
+
* SQLite commit per round-trip.
|
|
20953
|
+
*/
|
|
20954
|
+
lastContactAt: number$1().optional(),
|
|
20955
|
+
/**
|
|
20779
20956
|
* True when the source is a BINARY low-battery indicator (HA
|
|
20780
20957
|
* `binary_sensor` device_class=battery / `LOW_BAT`) that has no real
|
|
20781
20958
|
* charge level — `percentage` is then a coarse stand-in (100 = normal,
|
|
@@ -25053,6 +25230,16 @@ var CamStreamDescriptorSchema = object({
|
|
|
25053
25230
|
/** Transport-specific opaque metadata (e.g. rfc4571 SDP). */
|
|
25054
25231
|
metadata: record(string(), unknown()).optional()
|
|
25055
25232
|
});
|
|
25233
|
+
object({
|
|
25234
|
+
/** The descriptors as last built from a real camera response. Never a guess:
|
|
25235
|
+
* a failed or refused build writes NOTHING, so a restored catalog is always
|
|
25236
|
+
* one the camera itself once produced. */
|
|
25237
|
+
descriptors: array(CamStreamDescriptorSchema),
|
|
25238
|
+
/** Ms epoch of the build that produced {@link descriptors}. Lets the wake
|
|
25239
|
+
* path decide whether the camera's own awake window is worth spending on a
|
|
25240
|
+
* re-read. */
|
|
25241
|
+
lastFetchedAt: number$1()
|
|
25242
|
+
});
|
|
25056
25243
|
DeviceType.Camera, method(object({ deviceId: number$1().int().nonnegative() }), array(CamStreamDescriptorSchema).readonly());
|
|
25057
25244
|
/** One of the camera's stream profiles. */
|
|
25058
25245
|
var StreamProfileSchema = _enum([
|
|
@@ -28085,6 +28272,12 @@ Object.freeze({
|
|
|
28085
28272
|
addonId: null,
|
|
28086
28273
|
access: "view"
|
|
28087
28274
|
},
|
|
28275
|
+
"llm.resolveModelRef": {
|
|
28276
|
+
capName: "llm",
|
|
28277
|
+
capScope: "system",
|
|
28278
|
+
addonId: null,
|
|
28279
|
+
access: "create"
|
|
28280
|
+
},
|
|
28088
28281
|
"llm.setDefault": {
|
|
28089
28282
|
capName: "llm",
|
|
28090
28283
|
capScope: "system",
|
|
@@ -32477,6 +32670,11 @@ Object.freeze({
|
|
|
32477
32670
|
form: "single",
|
|
32478
32671
|
optional: false
|
|
32479
32672
|
}],
|
|
32673
|
+
"pipelineAnalytics.getEventMedia": [{
|
|
32674
|
+
name: "deviceId",
|
|
32675
|
+
form: "single",
|
|
32676
|
+
optional: false
|
|
32677
|
+
}],
|
|
32480
32678
|
"pipelineAnalytics.getKeyEvents": [{
|
|
32481
32679
|
name: "deviceId",
|
|
32482
32680
|
form: "single",
|
|
@@ -32507,6 +32705,11 @@ Object.freeze({
|
|
|
32507
32705
|
form: "single",
|
|
32508
32706
|
optional: false
|
|
32509
32707
|
}],
|
|
32708
|
+
"pipelineAnalytics.getTrackMedia": [{
|
|
32709
|
+
name: "deviceId",
|
|
32710
|
+
form: "single",
|
|
32711
|
+
optional: false
|
|
32712
|
+
}],
|
|
32510
32713
|
"pipelineAnalytics.getTrainingExportSummary": [{
|
|
32511
32714
|
name: "deviceIds",
|
|
32512
32715
|
form: "array",
|
|
@@ -32542,6 +32745,11 @@ Object.freeze({
|
|
|
32542
32745
|
form: "array",
|
|
32543
32746
|
optional: true
|
|
32544
32747
|
}],
|
|
32748
|
+
"pipelineAnalytics.listTrackMedia": [{
|
|
32749
|
+
name: "deviceId",
|
|
32750
|
+
form: "single",
|
|
32751
|
+
optional: false
|
|
32752
|
+
}],
|
|
32545
32753
|
"pipelineAnalytics.listTracks": [{
|
|
32546
32754
|
name: "deviceId",
|
|
32547
32755
|
form: "single",
|
|
@@ -32982,6 +33190,12 @@ Object.freeze({
|
|
|
32982
33190
|
form: "single",
|
|
32983
33191
|
optional: false
|
|
32984
33192
|
}],
|
|
33193
|
+
"snapshot.getSnapshotLinks": [{
|
|
33194
|
+
name: "targets",
|
|
33195
|
+
form: "object-array",
|
|
33196
|
+
optional: false,
|
|
33197
|
+
itemField: "deviceId"
|
|
33198
|
+
}],
|
|
32985
33199
|
"snapshot.getSnapshotOverview": [{
|
|
32986
33200
|
name: "deviceIds",
|
|
32987
33201
|
form: "array",
|
|
@@ -45793,7 +46007,7 @@ function inferDocMediaType(uriOrName) {
|
|
|
45793
46007
|
for (const [ext, media] of Object.entries(KNOWN_DOC_EXTENSIONS)) if (lower.endsWith(`.${ext}`)) return media;
|
|
45794
46008
|
return "application/octet-stream";
|
|
45795
46009
|
}
|
|
45796
|
-
function basename$
|
|
46010
|
+
function basename$2(uriOrName) {
|
|
45797
46011
|
const parts = uriOrName.split("/");
|
|
45798
46012
|
const last = parts[parts.length - 1];
|
|
45799
46013
|
return last && last.length > 0 ? last : void 0;
|
|
@@ -45823,7 +46037,7 @@ function annotationToSource({ annotation, generateId: generateId3 }) {
|
|
|
45823
46037
|
url: uri,
|
|
45824
46038
|
...fileCitation.file_name != null ? { title: fileCitation.file_name } : {}
|
|
45825
46039
|
};
|
|
45826
|
-
const filename = (_c = fileCitation.file_name) != null ? _c : basename$
|
|
46040
|
+
const filename = (_c = fileCitation.file_name) != null ? _c : basename$2(uri);
|
|
45827
46041
|
const mediaType = inferDocMediaType(uri);
|
|
45828
46042
|
return {
|
|
45829
46043
|
type: "source",
|
|
@@ -45912,7 +46126,7 @@ function builtinToolResultToSources({ block, generateId: generateId3 }) {
|
|
|
45912
46126
|
});
|
|
45913
46127
|
continue;
|
|
45914
46128
|
}
|
|
45915
|
-
const filename = (_h = entry.file_name) != null ? _h : basename$
|
|
46129
|
+
const filename = (_h = entry.file_name) != null ? _h : basename$2(uri);
|
|
45916
46130
|
const mediaType = inferDocMediaType(uri);
|
|
45917
46131
|
sources.push({
|
|
45918
46132
|
type: "source",
|
|
@@ -70119,6 +70333,469 @@ function resolveProfile(profiles, defaults, input) {
|
|
|
70119
70333
|
};
|
|
70120
70334
|
}
|
|
70121
70335
|
//#endregion
|
|
70336
|
+
//#region src/runtime/hf-ref.ts
|
|
70337
|
+
/**
|
|
70338
|
+
* Hugging Face model references — parse, then resolve against the HF API.
|
|
70339
|
+
*
|
|
70340
|
+
* The operator types ONE string and gets a fully-pinned download plan. That is
|
|
70341
|
+
* the whole surface: this is not an HF browser, and it deliberately cannot
|
|
70342
|
+
* discover a model for you — it can only turn a reference you already have
|
|
70343
|
+
* into something the node can fetch and verify.
|
|
70344
|
+
*
|
|
70345
|
+
* ## Why resolution can REFUSE
|
|
70346
|
+
*
|
|
70347
|
+
* A GGUF repo is not one model. `unsloth/Qwen3.6-35B-A3B-GGUF` ships 25
|
|
70348
|
+
* quantizations between 10 GB and 50 GB, and none of them is named `Q4_K_M`
|
|
70349
|
+
* (they are `UD-Q4_K_M`, Unsloth's dynamic quant). Any code that "defaults to
|
|
70350
|
+
* Q4_K_M" would either fail or, worse, pick a neighbouring file and hand the
|
|
70351
|
+
* operator a model they did not ask for after a 20 GB download. So: a repo
|
|
70352
|
+
* with more than one candidate is an ERROR that NAMES the candidates, never a
|
|
70353
|
+
* guess. The only silent pick is the mmproj precision (F16 over F32) — that
|
|
70354
|
+
* choice costs a few hundred MB of projector, not a different model, and the
|
|
70355
|
+
* file it picked is reported back.
|
|
70356
|
+
*
|
|
70357
|
+
* ## The error taxonomy is read from headers, not from the status
|
|
70358
|
+
*
|
|
70359
|
+
* Probed live on 2026-08-15: huggingface.co answers **401** both for a gated
|
|
70360
|
+
* repo and for a repo that does not exist (it refuses to leak whether a
|
|
70361
|
+
* private repo is there). The two are distinguishable only by
|
|
70362
|
+
* `x-error-code: GatedRepo`. Reading the status alone would tell a
|
|
70363
|
+
* typo'd repo name that it needs a token, which is the wrong instruction.
|
|
70364
|
+
*
|
|
70365
|
+
* ## What is verified before a byte is downloaded
|
|
70366
|
+
*
|
|
70367
|
+
* host is huggingface.co · extension is `.gguf` · every file exists in the
|
|
70368
|
+
* tree · the split-GGUF shard set is COMPLETE · the total (main + shards +
|
|
70369
|
+
* mmproj) is under the ceiling · a HEAD confirms the file is reachable with
|
|
70370
|
+
* the credentials at hand and that its size agrees with the tree. The sha256
|
|
70371
|
+
* comes free: HF's LFS `oid` IS the sha256 of the file, and `x-linked-etag`
|
|
70372
|
+
* repeats it on the HEAD.
|
|
70373
|
+
*/
|
|
70374
|
+
/** The only hosts a reference may point at. */
|
|
70375
|
+
var HF_HOSTS = ["huggingface.co", "www.huggingface.co"];
|
|
70376
|
+
var HF_API = "https://huggingface.co/api/models";
|
|
70377
|
+
var HF_RESOLVE = "https://huggingface.co";
|
|
70378
|
+
/** Where an operator puts a Hugging Face token, named in the gated error. */
|
|
70379
|
+
var HF_TOKEN_ENV = "HF_TOKEN";
|
|
70380
|
+
var SEGMENT_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
70381
|
+
function fail(code, message, candidates) {
|
|
70382
|
+
return {
|
|
70383
|
+
code,
|
|
70384
|
+
message,
|
|
70385
|
+
...candidates !== void 0 ? { candidates } : {}
|
|
70386
|
+
};
|
|
70387
|
+
}
|
|
70388
|
+
function badParse(code, message, candidates) {
|
|
70389
|
+
return {
|
|
70390
|
+
ok: false,
|
|
70391
|
+
error: fail(code, message, candidates)
|
|
70392
|
+
};
|
|
70393
|
+
}
|
|
70394
|
+
var EXPECTED = "expected https://huggingface.co/<org>/<repo>/resolve/main/<file>.gguf, or <org>/<repo>/<file>.gguf, or <org>/<repo>[:<QUANT>]";
|
|
70395
|
+
/**
|
|
70396
|
+
* Reference string → a repo/file reference. Pure: no network, no environment.
|
|
70397
|
+
* Every rejection names the form that WAS expected, because the operator is
|
|
70398
|
+
* pasting from a browser and a bare "invalid" tells them nothing.
|
|
70399
|
+
*/
|
|
70400
|
+
function parseHfRef(input) {
|
|
70401
|
+
const raw = input.trim();
|
|
70402
|
+
if (raw === "") return badParse("malformed", `empty model reference — ${EXPECTED}`);
|
|
70403
|
+
return raw.includes("://") ? parseUrlForm(raw) : parseBareForm(raw);
|
|
70404
|
+
}
|
|
70405
|
+
function parseUrlForm(raw) {
|
|
70406
|
+
let url;
|
|
70407
|
+
try {
|
|
70408
|
+
url = new URL(raw);
|
|
70409
|
+
} catch {
|
|
70410
|
+
return badParse("malformed", `not a URL: ${raw} — ${EXPECTED}`);
|
|
70411
|
+
}
|
|
70412
|
+
if (!HF_HOSTS.includes(url.hostname)) return badParse("not-huggingface", `only huggingface.co models can be installed this way; got host "${url.hostname}"`);
|
|
70413
|
+
const parts = url.pathname.split("/").filter((p) => p !== "");
|
|
70414
|
+
const marker = parts.findIndex((p) => p === "resolve" || p === "blob");
|
|
70415
|
+
if (marker !== 2 || parts.length < marker + 3) return badParse("malformed", `unrecognised Hugging Face URL: ${raw} — ${EXPECTED}`);
|
|
70416
|
+
return finishParse(`${String(parts[0])}/${String(parts[1])}`, String(parts[marker + 1]), parts.slice(marker + 2).join("/"), raw);
|
|
70417
|
+
}
|
|
70418
|
+
function parseBareForm(raw) {
|
|
70419
|
+
const [beforeTag, ...tagRest] = raw.split(":");
|
|
70420
|
+
const body = String(beforeTag);
|
|
70421
|
+
if (tagRest.length > 1) return badParse("malformed", `too many ":" in ${raw} — ${EXPECTED}`);
|
|
70422
|
+
const quant = tagRest[0]?.trim();
|
|
70423
|
+
const parts = body.split("/");
|
|
70424
|
+
if (parts.length < 2) return badParse("malformed", `not an <org>/<repo> reference: ${raw} — ${EXPECTED}`);
|
|
70425
|
+
if (parts.some((p) => p === "" || p === "." || p === "..")) return badParse("malformed", `illegal path segment in ${raw} — ${EXPECTED}`);
|
|
70426
|
+
const org = String(parts[0]);
|
|
70427
|
+
const name = String(parts[1]);
|
|
70428
|
+
if (!SEGMENT_RE.test(org) || !SEGMENT_RE.test(name)) return badParse("malformed", `illegal repo name in ${raw} — ${EXPECTED}`);
|
|
70429
|
+
const repo = `${org}/${name}`;
|
|
70430
|
+
if (parts.length === 2) {
|
|
70431
|
+
if (quant !== void 0 && quant === "") return badParse("malformed", `empty quantization tag in ${raw} — ${EXPECTED}`);
|
|
70432
|
+
return {
|
|
70433
|
+
ok: true,
|
|
70434
|
+
ref: {
|
|
70435
|
+
kind: "repo",
|
|
70436
|
+
repo,
|
|
70437
|
+
revision: "main",
|
|
70438
|
+
...quant !== void 0 ? { quant } : {}
|
|
70439
|
+
}
|
|
70440
|
+
};
|
|
70441
|
+
}
|
|
70442
|
+
if (quant !== void 0) return badParse("malformed", `a quantization tag cannot follow an explicit file: ${raw}`);
|
|
70443
|
+
return finishParse(repo, "main", parts.slice(2).join("/"), raw);
|
|
70444
|
+
}
|
|
70445
|
+
function finishParse(repo, revision, filePath, raw) {
|
|
70446
|
+
if (filePath.split("/").some((p) => p === "" || p === "." || p === "..")) return badParse("malformed", `illegal path segment in ${raw} — ${EXPECTED}`);
|
|
70447
|
+
if (!filePath.toLowerCase().endsWith(".gguf")) return badParse("not-gguf", `the managed local runtime loads GGUF only; "${filePath}" is not a .gguf file`);
|
|
70448
|
+
return {
|
|
70449
|
+
ok: true,
|
|
70450
|
+
ref: {
|
|
70451
|
+
kind: "file",
|
|
70452
|
+
repo,
|
|
70453
|
+
revision,
|
|
70454
|
+
filePath
|
|
70455
|
+
}
|
|
70456
|
+
};
|
|
70457
|
+
}
|
|
70458
|
+
/** `-00001-of-00002` — llama.cpp's split-GGUF naming. */
|
|
70459
|
+
var SHARD_RE = /^(.*)-(\d{5})-of-(\d{5})$/;
|
|
70460
|
+
/**
|
|
70461
|
+
* One `-`-delimited segment that is a quantization, e.g. `Q4_K_M`, `IQ2_XXS`,
|
|
70462
|
+
* `BF16`, `fp16`. The `FP` spellings are not cosmetic: `Qwen/*-GGUF` names its
|
|
70463
|
+
* unquantized file `…-fp16.gguf`, and a tag list that cannot name it offers
|
|
70464
|
+
* the operator a suggestion that does not parse.
|
|
70465
|
+
*/
|
|
70466
|
+
var QUANT_RE = /^(?:I?Q\d[A-Z0-9_]*|TQ\d_\d|BF16|FP?16|FP?32|FP8|MXFP4(?:_MOE)?)$/i;
|
|
70467
|
+
/** Shard coordinates of a split GGUF filename, or `null` when unsharded. */
|
|
70468
|
+
function shardInfoOf(filename) {
|
|
70469
|
+
const m = SHARD_RE.exec(stripGguf(filename));
|
|
70470
|
+
if (m === null) return null;
|
|
70471
|
+
return {
|
|
70472
|
+
stem: String(m[1]),
|
|
70473
|
+
index: Number(m[2]),
|
|
70474
|
+
total: Number(m[3])
|
|
70475
|
+
};
|
|
70476
|
+
}
|
|
70477
|
+
function stripGguf(filename) {
|
|
70478
|
+
return filename.replace(/\.gguf$/i, "");
|
|
70479
|
+
}
|
|
70480
|
+
/**
|
|
70481
|
+
* The quantization tag of a GGUF filename, uppercased, `UD-` prefix kept —
|
|
70482
|
+
* `''` when the name carries no recognisable tag. Shard coordinates are
|
|
70483
|
+
* stripped first so `X-BF16-00001-of-00002.gguf` reads as `BF16`.
|
|
70484
|
+
*/
|
|
70485
|
+
function quantizationOf(filename) {
|
|
70486
|
+
const segments = (shardInfoOf(filename)?.stem ?? stripGguf(filename)).split("-");
|
|
70487
|
+
for (let i = segments.length - 1; i >= 0; i--) {
|
|
70488
|
+
const seg = String(segments[i]);
|
|
70489
|
+
if (!QUANT_RE.test(seg)) continue;
|
|
70490
|
+
return (i > 0 ? String(segments[i - 1]) : "").toUpperCase() === "UD" ? `UD-${seg.toUpperCase()}` : seg.toUpperCase();
|
|
70491
|
+
}
|
|
70492
|
+
return "";
|
|
70493
|
+
}
|
|
70494
|
+
function isMmproj(filePath) {
|
|
70495
|
+
return basename$1(filePath).toLowerCase().startsWith("mmproj");
|
|
70496
|
+
}
|
|
70497
|
+
function basename$1(filePath) {
|
|
70498
|
+
return filePath.slice(filePath.lastIndexOf("/") + 1);
|
|
70499
|
+
}
|
|
70500
|
+
function dirname(filePath) {
|
|
70501
|
+
const i = filePath.lastIndexOf("/");
|
|
70502
|
+
return i < 0 ? "" : filePath.slice(0, i);
|
|
70503
|
+
}
|
|
70504
|
+
function headersFor(token) {
|
|
70505
|
+
return {
|
|
70506
|
+
"User-Agent": "CamStack/1.0",
|
|
70507
|
+
...token !== void 0 && token !== "" ? { Authorization: `Bearer ${token}` } : {}
|
|
70508
|
+
};
|
|
70509
|
+
}
|
|
70510
|
+
/** HF's 401-for-everything is only decodable through `x-error-code`. */
|
|
70511
|
+
function authError(response, repo) {
|
|
70512
|
+
const code = response.headers.get("x-error-code") ?? "";
|
|
70513
|
+
if (code === "GatedRepo" || code === "GatedRepoAccessDenied") return fail("gated", `${repo} is a gated Hugging Face repo: accept its licence with your HF account, then set a token in the ${HF_TOKEN_ENV} environment variable on the hub and on the runtime node (${HF_TOKEN_ENV} or HUGGING_FACE_HUB_TOKEN), and restart them.`);
|
|
70514
|
+
return fail("repo-not-found", `${repo} does not exist on huggingface.co, or is private (Hugging Face answers 401 for both). Check the org/repo spelling.`);
|
|
70515
|
+
}
|
|
70516
|
+
async function readTree(ref, fetchFn, token) {
|
|
70517
|
+
const url = `${HF_API}/${ref.repo}/tree/${ref.revision}?recursive=1`;
|
|
70518
|
+
let response;
|
|
70519
|
+
try {
|
|
70520
|
+
response = await fetchFn(url, {
|
|
70521
|
+
method: "GET",
|
|
70522
|
+
headers: headersFor(token)
|
|
70523
|
+
});
|
|
70524
|
+
} catch (err) {
|
|
70525
|
+
return {
|
|
70526
|
+
ok: false,
|
|
70527
|
+
error: fail("network", `could not reach huggingface.co: ${message(err)}`)
|
|
70528
|
+
};
|
|
70529
|
+
}
|
|
70530
|
+
if (response.status === 401 || response.status === 403) return {
|
|
70531
|
+
ok: false,
|
|
70532
|
+
error: authError(response, ref.repo)
|
|
70533
|
+
};
|
|
70534
|
+
if (response.status === 404) return {
|
|
70535
|
+
ok: false,
|
|
70536
|
+
error: fail("repo-not-found", `${ref.repo} has no revision "${ref.revision}"`)
|
|
70537
|
+
};
|
|
70538
|
+
if (!response.ok) return {
|
|
70539
|
+
ok: false,
|
|
70540
|
+
error: fail("network", `huggingface.co answered ${String(response.status)} for ${ref.repo}`)
|
|
70541
|
+
};
|
|
70542
|
+
let body;
|
|
70543
|
+
try {
|
|
70544
|
+
body = await response.json();
|
|
70545
|
+
} catch (err) {
|
|
70546
|
+
return {
|
|
70547
|
+
ok: false,
|
|
70548
|
+
error: fail("network", `unreadable tree for ${ref.repo}: ${message(err)}`)
|
|
70549
|
+
};
|
|
70550
|
+
}
|
|
70551
|
+
if (!Array.isArray(body)) return {
|
|
70552
|
+
ok: false,
|
|
70553
|
+
error: fail("network", `unexpected tree payload for ${ref.repo}`)
|
|
70554
|
+
};
|
|
70555
|
+
return {
|
|
70556
|
+
ok: true,
|
|
70557
|
+
files: body.map(toTreeEntry).filter((e) => e !== null)
|
|
70558
|
+
};
|
|
70559
|
+
}
|
|
70560
|
+
function toTreeEntry(raw) {
|
|
70561
|
+
if (typeof raw !== "object" || raw === null) return null;
|
|
70562
|
+
const record = { ...raw };
|
|
70563
|
+
if (record["type"] !== "file") return null;
|
|
70564
|
+
const filePath = record["path"];
|
|
70565
|
+
if (typeof filePath !== "string" || !filePath.toLowerCase().endsWith(".gguf")) return null;
|
|
70566
|
+
const lfs = typeof record["lfs"] === "object" && record["lfs"] !== null ? { ...record["lfs"] } : {};
|
|
70567
|
+
const lfsSize = lfs["size"];
|
|
70568
|
+
const oid = lfs["oid"];
|
|
70569
|
+
const plainSize = record["size"];
|
|
70570
|
+
return {
|
|
70571
|
+
path: filePath,
|
|
70572
|
+
sizeBytes: typeof lfsSize === "number" ? lfsSize : typeof plainSize === "number" ? plainSize : 0,
|
|
70573
|
+
...typeof oid === "string" && oid.length === 64 ? { sha256: oid } : {}
|
|
70574
|
+
};
|
|
70575
|
+
}
|
|
70576
|
+
function message(err) {
|
|
70577
|
+
return err instanceof Error ? err.message : String(err);
|
|
70578
|
+
}
|
|
70579
|
+
/** Files that can be THE model: not a projector, not a follow-on shard. */
|
|
70580
|
+
function modelCandidates(files) {
|
|
70581
|
+
return files.filter((f) => {
|
|
70582
|
+
if (isMmproj(f.path)) return false;
|
|
70583
|
+
const shard = shardInfoOf(basename$1(f.path));
|
|
70584
|
+
return shard === null || shard.index === 1;
|
|
70585
|
+
});
|
|
70586
|
+
}
|
|
70587
|
+
function labelFor(file) {
|
|
70588
|
+
const quant = quantizationOf(basename$1(file.path));
|
|
70589
|
+
return quant === "" ? basename$1(file.path) : quant;
|
|
70590
|
+
}
|
|
70591
|
+
function selectMain(ref, files) {
|
|
70592
|
+
const candidates = modelCandidates(files);
|
|
70593
|
+
if (ref.kind === "file") {
|
|
70594
|
+
const wanted = ref.filePath.toLowerCase();
|
|
70595
|
+
const hit = files.find((f) => f.path.toLowerCase() === wanted);
|
|
70596
|
+
if (hit === void 0) return {
|
|
70597
|
+
ok: false,
|
|
70598
|
+
error: fail("file-not-found", `${ref.repo} has no file "${ref.filePath}" at revision ${ref.revision}`, candidates.map(labelFor))
|
|
70599
|
+
};
|
|
70600
|
+
return {
|
|
70601
|
+
ok: true,
|
|
70602
|
+
file: hit
|
|
70603
|
+
};
|
|
70604
|
+
}
|
|
70605
|
+
if (candidates.length === 0) return {
|
|
70606
|
+
ok: false,
|
|
70607
|
+
error: fail("not-gguf", `${ref.repo} publishes no GGUF weights (only projectors or no GGUF at all)`)
|
|
70608
|
+
};
|
|
70609
|
+
if (ref.quant !== void 0) {
|
|
70610
|
+
const wanted = ref.quant.toUpperCase();
|
|
70611
|
+
const wantedFile = stripGguf(ref.quant).toUpperCase();
|
|
70612
|
+
const matches = candidates.filter((f) => quantizationOf(basename$1(f.path)) === wanted || stripGguf(basename$1(f.path)).toUpperCase() === wantedFile);
|
|
70613
|
+
if (matches.length === 0) return {
|
|
70614
|
+
ok: false,
|
|
70615
|
+
error: fail("file-not-found", `${ref.repo} has no "${ref.quant}" quantization. Available: ${candidates.map(labelFor).join(", ")}`, dedupe(candidates.map(labelFor)))
|
|
70616
|
+
};
|
|
70617
|
+
const only = matches[0];
|
|
70618
|
+
if (matches.length > 1 || only === void 0) return {
|
|
70619
|
+
ok: false,
|
|
70620
|
+
error: fail("ambiguous", `"${ref.quant}" matches ${String(matches.length)} files in ${ref.repo}: ${matches.map((f) => basename$1(f.path)).join(", ")}. Name the file explicitly.`, matches.map((f) => basename$1(f.path)))
|
|
70621
|
+
};
|
|
70622
|
+
return {
|
|
70623
|
+
ok: true,
|
|
70624
|
+
file: only
|
|
70625
|
+
};
|
|
70626
|
+
}
|
|
70627
|
+
const solo = candidates[0];
|
|
70628
|
+
if (candidates.length > 1 || solo === void 0) {
|
|
70629
|
+
const tags = dedupe(candidates.map(labelFor));
|
|
70630
|
+
return {
|
|
70631
|
+
ok: false,
|
|
70632
|
+
error: fail("ambiguous", `${ref.repo} publishes ${String(candidates.length)} quantizations and picking one for you would be a guess. Re-enter it as ${ref.repo}:<TAG>, or paste the full file URL. Available: ${tags.join(", ")}`, tags)
|
|
70633
|
+
};
|
|
70634
|
+
}
|
|
70635
|
+
return {
|
|
70636
|
+
ok: true,
|
|
70637
|
+
file: solo
|
|
70638
|
+
};
|
|
70639
|
+
}
|
|
70640
|
+
function dedupe(values) {
|
|
70641
|
+
return [...new Set(values)];
|
|
70642
|
+
}
|
|
70643
|
+
/** Shards 2..N of `main`, or an error naming the first one that is missing. */
|
|
70644
|
+
function collectShards(main, files) {
|
|
70645
|
+
const shard = shardInfoOf(basename$1(main.path));
|
|
70646
|
+
if (shard === null || shard.total <= 1) return {
|
|
70647
|
+
ok: true,
|
|
70648
|
+
shards: []
|
|
70649
|
+
};
|
|
70650
|
+
const dir = dirname(main.path);
|
|
70651
|
+
const out = [];
|
|
70652
|
+
for (let i = 2; i <= shard.total; i++) {
|
|
70653
|
+
const wanted = `${shard.stem}-${String(i).padStart(5, "0")}-of-${String(shard.total).padStart(5, "0")}.gguf`;
|
|
70654
|
+
const full = dir === "" ? wanted : `${dir}/${wanted}`;
|
|
70655
|
+
const hit = files.find((f) => f.path === full);
|
|
70656
|
+
if (hit === void 0) return {
|
|
70657
|
+
ok: false,
|
|
70658
|
+
error: fail("incomplete-shards", `split GGUF is incomplete: ${wanted} is missing from the repo (llama.cpp needs all ${String(shard.total)} shards)`)
|
|
70659
|
+
};
|
|
70660
|
+
out.push(hit);
|
|
70661
|
+
}
|
|
70662
|
+
return {
|
|
70663
|
+
ok: true,
|
|
70664
|
+
shards: out
|
|
70665
|
+
};
|
|
70666
|
+
}
|
|
70667
|
+
/** F16 over BF16 over F32 over whatever came first — reported, never hidden. */
|
|
70668
|
+
var MMPROJ_PREFERENCE = [
|
|
70669
|
+
"F16",
|
|
70670
|
+
"BF16",
|
|
70671
|
+
"F32"
|
|
70672
|
+
];
|
|
70673
|
+
function selectMmproj(files) {
|
|
70674
|
+
const projectors = files.filter((f) => isMmproj(f.path));
|
|
70675
|
+
if (projectors.length === 0) return null;
|
|
70676
|
+
for (const want of MMPROJ_PREFERENCE) {
|
|
70677
|
+
const hit = projectors.find((f) => quantizationOf(basename$1(f.path)) === want);
|
|
70678
|
+
if (hit !== void 0) return hit;
|
|
70679
|
+
}
|
|
70680
|
+
return projectors[0] ?? null;
|
|
70681
|
+
}
|
|
70682
|
+
function resolveUrl(repo, revision, filePath) {
|
|
70683
|
+
return `${HF_RESOLVE}/${repo}/resolve/${revision}/${filePath}`;
|
|
70684
|
+
}
|
|
70685
|
+
async function verifyHead(url, repo, declaredBytes, fetchFn, token) {
|
|
70686
|
+
let response;
|
|
70687
|
+
try {
|
|
70688
|
+
response = await fetchFn(url, {
|
|
70689
|
+
method: "HEAD",
|
|
70690
|
+
redirect: "manual",
|
|
70691
|
+
headers: headersFor(token)
|
|
70692
|
+
});
|
|
70693
|
+
} catch (err) {
|
|
70694
|
+
return {
|
|
70695
|
+
ok: false,
|
|
70696
|
+
error: fail("network", `HEAD ${url} failed: ${message(err)}`)
|
|
70697
|
+
};
|
|
70698
|
+
}
|
|
70699
|
+
if (response.status === 401 || response.status === 403) return {
|
|
70700
|
+
ok: false,
|
|
70701
|
+
error: authError(response, repo)
|
|
70702
|
+
};
|
|
70703
|
+
if (response.status === 404) return {
|
|
70704
|
+
ok: false,
|
|
70705
|
+
error: fail("file-not-found", `${url} is gone (404)`)
|
|
70706
|
+
};
|
|
70707
|
+
if (response.status >= 400) return {
|
|
70708
|
+
ok: false,
|
|
70709
|
+
error: fail("network", `HEAD ${url} answered ${String(response.status)}`)
|
|
70710
|
+
};
|
|
70711
|
+
const linked = response.headers.get("x-linked-size") ?? response.headers.get("content-length");
|
|
70712
|
+
const headBytes = linked === null ? void 0 : Number(linked);
|
|
70713
|
+
if (headBytes !== void 0 && Number.isFinite(headBytes) && headBytes !== declaredBytes) return {
|
|
70714
|
+
ok: false,
|
|
70715
|
+
error: fail("size-mismatch", `huggingface.co reports ${String(headBytes)} bytes for ${url} but its tree declared ${String(declaredBytes)} — refusing to download a file that changed under the reference`)
|
|
70716
|
+
};
|
|
70717
|
+
const etag = response.headers.get("x-linked-etag")?.replace(/"/g, "");
|
|
70718
|
+
return {
|
|
70719
|
+
ok: true,
|
|
70720
|
+
...etag !== void 0 && etag.length === 64 ? { sha256: etag } : {}
|
|
70721
|
+
};
|
|
70722
|
+
}
|
|
70723
|
+
function toResolved(repo, revision, entry) {
|
|
70724
|
+
return {
|
|
70725
|
+
url: resolveUrl(repo, revision, entry.path),
|
|
70726
|
+
filename: basename$1(entry.path),
|
|
70727
|
+
sizeBytes: entry.sizeBytes,
|
|
70728
|
+
...entry.sha256 !== void 0 ? { sha256: entry.sha256 } : {}
|
|
70729
|
+
};
|
|
70730
|
+
}
|
|
70731
|
+
function gb$1(bytes) {
|
|
70732
|
+
return `${(bytes / 1e9).toFixed(1)} GB`;
|
|
70733
|
+
}
|
|
70734
|
+
/** Reference → a pinned, size-checked, HEAD-verified download plan. */
|
|
70735
|
+
async function resolveHfRef(ref, deps) {
|
|
70736
|
+
const fetchFn = deps.fetchFn ?? fetch;
|
|
70737
|
+
const maxBytes = deps.maxBytes ?? 21474836480;
|
|
70738
|
+
const tree = await readTree(ref, fetchFn, deps.token);
|
|
70739
|
+
if (!tree.ok) return {
|
|
70740
|
+
ok: false,
|
|
70741
|
+
error: tree.error
|
|
70742
|
+
};
|
|
70743
|
+
const picked = selectMain(ref, tree.files);
|
|
70744
|
+
if (!picked.ok) return {
|
|
70745
|
+
ok: false,
|
|
70746
|
+
error: picked.error
|
|
70747
|
+
};
|
|
70748
|
+
const main = picked.file;
|
|
70749
|
+
const shards = collectShards(main, tree.files);
|
|
70750
|
+
if (!shards.ok) return {
|
|
70751
|
+
ok: false,
|
|
70752
|
+
error: shards.error
|
|
70753
|
+
};
|
|
70754
|
+
const projector = isMmproj(main.path) ? null : selectMmproj(tree.files);
|
|
70755
|
+
const extraEntries = [...shards.shards, ...projector === null ? [] : [projector]];
|
|
70756
|
+
const totalBytes = [main, ...extraEntries].reduce((sum, f) => sum + f.sizeBytes, 0);
|
|
70757
|
+
if (totalBytes > maxBytes) return {
|
|
70758
|
+
ok: false,
|
|
70759
|
+
error: {
|
|
70760
|
+
...fail("too-large", `this install is ${String(totalBytes)} bytes (${gb$1(totalBytes)}), over the ${String(maxBytes)} byte ceiling (${gb$1(maxBytes)}). Raise the ceiling explicitly if the node really has the disk and RAM for it.`),
|
|
70761
|
+
requiredBytes: totalBytes
|
|
70762
|
+
}
|
|
70763
|
+
};
|
|
70764
|
+
const head = await verifyHead(resolveUrl(ref.repo, ref.revision, main.path), ref.repo, main.sizeBytes, fetchFn, deps.token);
|
|
70765
|
+
if (!head.ok) return {
|
|
70766
|
+
ok: false,
|
|
70767
|
+
error: head.error
|
|
70768
|
+
};
|
|
70769
|
+
const mainResolved = toResolved(ref.repo, ref.revision, {
|
|
70770
|
+
...main,
|
|
70771
|
+
...main.sha256 === void 0 && head.sha256 !== void 0 ? { sha256: head.sha256 } : {}
|
|
70772
|
+
});
|
|
70773
|
+
const quantization = quantizationOf(mainResolved.filename);
|
|
70774
|
+
const repoName = ref.repo.slice(ref.repo.indexOf("/") + 1);
|
|
70775
|
+
return {
|
|
70776
|
+
ok: true,
|
|
70777
|
+
resolution: {
|
|
70778
|
+
repo: ref.repo,
|
|
70779
|
+
revision: ref.revision,
|
|
70780
|
+
label: quantization === "" ? repoName : `${repoName} · ${quantization}`,
|
|
70781
|
+
quantization,
|
|
70782
|
+
purpose: projector === null ? "text" : "vision",
|
|
70783
|
+
main: mainResolved,
|
|
70784
|
+
extras: extraEntries.map((e) => toResolved(ref.repo, ref.revision, e)),
|
|
70785
|
+
totalBytes
|
|
70786
|
+
}
|
|
70787
|
+
};
|
|
70788
|
+
}
|
|
70789
|
+
/** `parseHfRef` then {@link resolveHfRef} — the form the cap method calls. */
|
|
70790
|
+
async function resolveHfReference(input, deps) {
|
|
70791
|
+
const parsed = parseHfRef(input);
|
|
70792
|
+
if (!parsed.ok) return {
|
|
70793
|
+
ok: false,
|
|
70794
|
+
error: parsed.error
|
|
70795
|
+
};
|
|
70796
|
+
return resolveHfRef(parsed.ref, deps);
|
|
70797
|
+
}
|
|
70798
|
+
//#endregion
|
|
70122
70799
|
//#region src/secrets.ts
|
|
70123
70800
|
/** Same marker as addon-notifiers/src/secrets.ts — UI contract. */
|
|
70124
70801
|
var REDACTED_MARKER = "__redacted__";
|
|
@@ -70342,6 +71019,64 @@ function createLlmProvider(deps) {
|
|
|
70342
71019
|
}));
|
|
70343
71020
|
},
|
|
70344
71021
|
listNodeModels: async ({ nodeId }) => requireRuntime(deps.runtime).listLocalModels(nodeId),
|
|
71022
|
+
/**
|
|
71023
|
+
* Hugging Face reference → a pinned `ManagedModelRef`, on the HUB.
|
|
71024
|
+
*
|
|
71025
|
+
* Never throws: a refusal ("this repo has 24 quantizations", "this is
|
|
71026
|
+
* gated", "23 GB is over the ceiling") is an ANSWER the operator has to
|
|
71027
|
+
* read and act on, and turning it into a tRPC error would reduce all of
|
|
71028
|
+
* them to a red toast with no candidate list and no override.
|
|
71029
|
+
*/
|
|
71030
|
+
resolveModelRef: async ({ ref, maxBytes }) => {
|
|
71031
|
+
const token = deps.hfToken?.();
|
|
71032
|
+
const outcome = await resolveHfReference(ref, {
|
|
71033
|
+
...maxBytes !== void 0 ? { maxBytes } : {},
|
|
71034
|
+
...token !== void 0 && token !== "" ? { token } : {}
|
|
71035
|
+
});
|
|
71036
|
+
if (!outcome.ok) {
|
|
71037
|
+
deps.logger?.info("llm model reference refused", { meta: {
|
|
71038
|
+
ref,
|
|
71039
|
+
code: outcome.error.code
|
|
71040
|
+
} });
|
|
71041
|
+
return {
|
|
71042
|
+
ok: false,
|
|
71043
|
+
code: outcome.error.code,
|
|
71044
|
+
message: outcome.error.message,
|
|
71045
|
+
...outcome.error.candidates !== void 0 ? { candidates: [...outcome.error.candidates] } : {},
|
|
71046
|
+
...outcome.error.requiredBytes !== void 0 ? { requiredBytes: outcome.error.requiredBytes } : {}
|
|
71047
|
+
};
|
|
71048
|
+
}
|
|
71049
|
+
const { resolution } = outcome;
|
|
71050
|
+
deps.logger?.info("llm model reference resolved", { meta: {
|
|
71051
|
+
ref,
|
|
71052
|
+
repo: resolution.repo,
|
|
71053
|
+
quantization: resolution.quantization,
|
|
71054
|
+
purpose: resolution.purpose,
|
|
71055
|
+
totalBytes: resolution.totalBytes
|
|
71056
|
+
} });
|
|
71057
|
+
return {
|
|
71058
|
+
ok: true,
|
|
71059
|
+
model: {
|
|
71060
|
+
kind: "url",
|
|
71061
|
+
url: resolution.main.url,
|
|
71062
|
+
...resolution.main.sha256 !== void 0 ? { sha256: resolution.main.sha256 } : {},
|
|
71063
|
+
label: resolution.label,
|
|
71064
|
+
sizeBytes: resolution.main.sizeBytes,
|
|
71065
|
+
extraFiles: resolution.extras.map((e) => ({
|
|
71066
|
+
url: e.url,
|
|
71067
|
+
filename: e.filename,
|
|
71068
|
+
sizeBytes: e.sizeBytes,
|
|
71069
|
+
...e.sha256 !== void 0 ? { sha256: e.sha256 } : {}
|
|
71070
|
+
}))
|
|
71071
|
+
},
|
|
71072
|
+
label: resolution.label,
|
|
71073
|
+
repo: resolution.repo,
|
|
71074
|
+
quantization: resolution.quantization,
|
|
71075
|
+
purpose: resolution.purpose,
|
|
71076
|
+
totalBytes: resolution.totalBytes,
|
|
71077
|
+
extraFilenames: resolution.extras.map((e) => e.filename)
|
|
71078
|
+
};
|
|
71079
|
+
},
|
|
70345
71080
|
installModel: async ({ nodeId, model }) => {
|
|
70346
71081
|
const runtime = requireRuntime(deps.runtime);
|
|
70347
71082
|
try {
|
|
@@ -70375,13 +71110,29 @@ function createLlmProvider(deps) {
|
|
|
70375
71110
|
//#endregion
|
|
70376
71111
|
//#region src/runtime/llm-model-catalog.ts
|
|
70377
71112
|
/**
|
|
70378
|
-
* Curated managed-model catalog (operator decision #4)
|
|
70379
|
-
*
|
|
70380
|
-
*
|
|
70381
|
-
*
|
|
70382
|
-
*
|
|
70383
|
-
*
|
|
70384
|
-
*
|
|
71113
|
+
* Curated managed-model catalog (operator decision #4). Each entry carries BOTH
|
|
71114
|
+
* the LLM-facing picker view (`meta`) and the REUSED download-plane
|
|
71115
|
+
* `ModelCatalogEntry` (`entry`) so GGUFs ride `ensureModel` +
|
|
71116
|
+
* `model-distributor` untouched — no bespoke fetcher (spec §4.2).
|
|
71117
|
+
* Digests/sizes pinned via scratchpad/pin-llm-models.mjs (HF LFS `lfs.oid`,
|
|
71118
|
+
* which IS the file's sha256).
|
|
71119
|
+
*
|
|
71120
|
+
* ## Two tiers, and `minRamBytes` is what separates them
|
|
71121
|
+
*
|
|
71122
|
+
* The first three entries are sized to the WEAKEST runtime node (the N100
|
|
71123
|
+
* agent): 1-4 GB, Q4. `QWEN36_35B` is not — it is 23 GB and only a big node
|
|
71124
|
+
* can hold it. The catalog does not refuse to show it; `minRamBytes` is the
|
|
71125
|
+
* guidance, and the picker prints the size. Keeping the tiers in one list is
|
|
71126
|
+
* deliberate: an operator with a 64 GB box should not have to discover the
|
|
71127
|
+
* free-text field to run something real.
|
|
71128
|
+
*
|
|
71129
|
+
* ## This list is no longer the boundary of what can run
|
|
71130
|
+
*
|
|
71131
|
+
* Anything on Hugging Face is installable through `llm.resolveModelRef` +
|
|
71132
|
+
* `installModel` without a code change ({@link ./hf-ref.ts}). An entry here
|
|
71133
|
+
* buys exactly two things over typing the reference: a pinned digest nobody
|
|
71134
|
+
* has to re-verify, and a `contextSizeDefault`/`minRamBytes` somebody checked.
|
|
71135
|
+
* Add one only when both are true.
|
|
70385
71136
|
*/
|
|
70386
71137
|
var GIB = 1024 * 1024 * 1024;
|
|
70387
71138
|
function mb(bytes) {
|
|
@@ -70433,30 +71184,117 @@ var LLAMA = textEntry({
|
|
|
70433
71184
|
var SMOLVLM_MODEL_BYTES = 1112602656;
|
|
70434
71185
|
var SMOLVLM_MMPROJ_BYTES = 872303680;
|
|
70435
71186
|
var SMOLVLM_MMPROJ_URL = "https://huggingface.co/ggml-org/SmolVLM2-2.2B-Instruct-GGUF/resolve/main/mmproj-SmolVLM2-2.2B-Instruct-f16.gguf";
|
|
71187
|
+
var SMOLVLM = {
|
|
71188
|
+
meta: {
|
|
71189
|
+
id: "llm-smolvlm2-2.2b-instruct-q4",
|
|
71190
|
+
label: "SmolVLM2 2.2B Instruct (vision)",
|
|
71191
|
+
family: "smolvlm2",
|
|
71192
|
+
purpose: "vision",
|
|
71193
|
+
url: "https://huggingface.co/ggml-org/SmolVLM2-2.2B-Instruct-GGUF/resolve/main/SmolVLM2-2.2B-Instruct-Q4_K_M.gguf",
|
|
71194
|
+
sha256: "0cf76814555b8665149075b74ab6b5c1d428ea1d3d01c1918c12012e8d7c9f58",
|
|
71195
|
+
sizeBytes: SMOLVLM_MODEL_BYTES,
|
|
71196
|
+
quantization: "Q4_K_M",
|
|
71197
|
+
minRamBytes: 4 * GIB,
|
|
71198
|
+
contextSizeDefault: 4096,
|
|
71199
|
+
mmprojUrl: SMOLVLM_MMPROJ_URL
|
|
71200
|
+
},
|
|
71201
|
+
entry: {
|
|
71202
|
+
id: "llm-smolvlm2-2.2b-instruct-q4",
|
|
71203
|
+
name: "SmolVLM2 2.2B Instruct (vision)",
|
|
71204
|
+
description: "smolvlm2 · Q4_K_M · +mmproj",
|
|
71205
|
+
formats: { gguf: {
|
|
71206
|
+
url: "https://huggingface.co/ggml-org/SmolVLM2-2.2B-Instruct-GGUF/resolve/main/SmolVLM2-2.2B-Instruct-Q4_K_M.gguf",
|
|
71207
|
+
sizeMB: mb(SMOLVLM_MODEL_BYTES)
|
|
71208
|
+
} },
|
|
71209
|
+
inputSize: {
|
|
71210
|
+
width: 0,
|
|
71211
|
+
height: 0
|
|
71212
|
+
},
|
|
71213
|
+
labels: [],
|
|
71214
|
+
extraFiles: [{
|
|
71215
|
+
url: SMOLVLM_MMPROJ_URL,
|
|
71216
|
+
filename: "mmproj-SmolVLM2-2.2B-Instruct-f16.gguf",
|
|
71217
|
+
sizeMB: mb(SMOLVLM_MMPROJ_BYTES)
|
|
71218
|
+
}]
|
|
71219
|
+
}
|
|
71220
|
+
};
|
|
71221
|
+
var QWEN3VL2B_MODEL_BYTES = 1107410624;
|
|
71222
|
+
var QWEN3VL2B_MMPROJ_BYTES = 819395232;
|
|
71223
|
+
var QWEN3VL2B_BASE = "https://huggingface.co/unsloth/Qwen3-VL-2B-Instruct-GGUF/resolve/main";
|
|
71224
|
+
var QWEN3VL2B_MMPROJ_URL = `${QWEN3VL2B_BASE}/mmproj-F16.gguf`;
|
|
71225
|
+
var QWEN3VL2B_URL = `${QWEN3VL2B_BASE}/Qwen3-VL-2B-Instruct-Q4_K_M.gguf`;
|
|
71226
|
+
/**
|
|
71227
|
+
* The light vision tier the operator asked for by weight class (~2 GB all in):
|
|
71228
|
+
* same Qwen3-VL family as the LM Studio 8B profile already in daily use, so
|
|
71229
|
+
* prompts and behaviour carry over — at a tenth of the 35B's disk and a RAM
|
|
71230
|
+
* floor a hub-adjacent node can always afford. This is the sensible default
|
|
71231
|
+
* for the NC confirm gates and summary judges.
|
|
71232
|
+
*/
|
|
71233
|
+
var QWEN3VL_2B = {
|
|
71234
|
+
meta: {
|
|
71235
|
+
id: "llm-qwen3-vl-2b-instruct-q4",
|
|
71236
|
+
label: "Qwen3-VL 2B Instruct (vision, light)",
|
|
71237
|
+
family: "qwen3-vl",
|
|
71238
|
+
purpose: "vision",
|
|
71239
|
+
url: QWEN3VL2B_URL,
|
|
71240
|
+
sha256: "858fcf2a39dc73b26dd86592cb0a5f949b59d1edb365d1dea98e46b02e955e56",
|
|
71241
|
+
sizeBytes: QWEN3VL2B_MODEL_BYTES,
|
|
71242
|
+
quantization: "Q4_K_M",
|
|
71243
|
+
minRamBytes: 3 * GIB,
|
|
71244
|
+
contextSizeDefault: 8192,
|
|
71245
|
+
mmprojUrl: QWEN3VL2B_MMPROJ_URL
|
|
71246
|
+
},
|
|
71247
|
+
entry: {
|
|
71248
|
+
id: "llm-qwen3-vl-2b-instruct-q4",
|
|
71249
|
+
name: "Qwen3-VL 2B Instruct (vision, light)",
|
|
71250
|
+
description: "qwen3-vl · Q4_K_M · +mmproj",
|
|
71251
|
+
formats: { gguf: {
|
|
71252
|
+
url: QWEN3VL2B_URL,
|
|
71253
|
+
sizeMB: mb(QWEN3VL2B_MODEL_BYTES)
|
|
71254
|
+
} },
|
|
71255
|
+
inputSize: {
|
|
71256
|
+
width: 0,
|
|
71257
|
+
height: 0
|
|
71258
|
+
},
|
|
71259
|
+
labels: [],
|
|
71260
|
+
extraFiles: [{
|
|
71261
|
+
url: QWEN3VL2B_MMPROJ_URL,
|
|
71262
|
+
filename: "mmproj-F16.gguf",
|
|
71263
|
+
sizeMB: mb(QWEN3VL2B_MMPROJ_BYTES)
|
|
71264
|
+
}]
|
|
71265
|
+
}
|
|
71266
|
+
};
|
|
71267
|
+
var QWEN36_MODEL_BYTES = 22134528992;
|
|
71268
|
+
var QWEN36_MMPROJ_BYTES = 899283680;
|
|
71269
|
+
var QWEN36_BASE = "https://huggingface.co/unsloth/Qwen3.6-35B-A3B-GGUF/resolve/main";
|
|
71270
|
+
var QWEN36_MMPROJ_URL = `${QWEN36_BASE}/mmproj-F16.gguf`;
|
|
71271
|
+
var QWEN36_URL = `${QWEN36_BASE}/Qwen3.6-35B-A3B-UD-Q4_K_M.gguf`;
|
|
70436
71272
|
var LLM_MODEL_CATALOG = [
|
|
70437
71273
|
QWEN,
|
|
70438
71274
|
LLAMA,
|
|
71275
|
+
SMOLVLM,
|
|
71276
|
+
QWEN3VL_2B,
|
|
70439
71277
|
{
|
|
70440
71278
|
meta: {
|
|
70441
|
-
id: "llm-
|
|
70442
|
-
label: "
|
|
70443
|
-
family: "
|
|
71279
|
+
id: "llm-qwen3.6-35b-a3b-ud-q4",
|
|
71280
|
+
label: "Qwen3.6 35B-A3B (vision)",
|
|
71281
|
+
family: "qwen3.6",
|
|
70444
71282
|
purpose: "vision",
|
|
70445
|
-
url:
|
|
70446
|
-
sha256: "
|
|
70447
|
-
sizeBytes:
|
|
70448
|
-
quantization: "Q4_K_M",
|
|
70449
|
-
minRamBytes:
|
|
70450
|
-
contextSizeDefault:
|
|
70451
|
-
mmprojUrl:
|
|
71283
|
+
url: QWEN36_URL,
|
|
71284
|
+
sha256: "ac0e2c1189e055faa36eff361580e79c5bd6f8e76bffb4ce547f167d53e31a61",
|
|
71285
|
+
sizeBytes: QWEN36_MODEL_BYTES,
|
|
71286
|
+
quantization: "UD-Q4_K_M",
|
|
71287
|
+
minRamBytes: 26 * GIB,
|
|
71288
|
+
contextSizeDefault: 32768,
|
|
71289
|
+
mmprojUrl: QWEN36_MMPROJ_URL
|
|
70452
71290
|
},
|
|
70453
71291
|
entry: {
|
|
70454
|
-
id: "llm-
|
|
70455
|
-
name: "
|
|
70456
|
-
description: "
|
|
71292
|
+
id: "llm-qwen3.6-35b-a3b-ud-q4",
|
|
71293
|
+
name: "Qwen3.6 35B-A3B (vision)",
|
|
71294
|
+
description: "qwen3.6 · UD-Q4_K_M · +mmproj",
|
|
70457
71295
|
formats: { gguf: {
|
|
70458
|
-
url:
|
|
70459
|
-
sizeMB: mb(
|
|
71296
|
+
url: QWEN36_URL,
|
|
71297
|
+
sizeMB: mb(QWEN36_MODEL_BYTES)
|
|
70460
71298
|
} },
|
|
70461
71299
|
inputSize: {
|
|
70462
71300
|
width: 0,
|
|
@@ -70464,9 +71302,9 @@ var LLM_MODEL_CATALOG = [
|
|
|
70464
71302
|
},
|
|
70465
71303
|
labels: [],
|
|
70466
71304
|
extraFiles: [{
|
|
70467
|
-
url:
|
|
70468
|
-
filename: "mmproj-
|
|
70469
|
-
sizeMB: mb(
|
|
71305
|
+
url: QWEN36_MMPROJ_URL,
|
|
71306
|
+
filename: "mmproj-F16.gguf",
|
|
71307
|
+
sizeMB: mb(QWEN36_MMPROJ_BYTES)
|
|
70470
71308
|
}]
|
|
70471
71309
|
}
|
|
70472
71310
|
}
|
|
@@ -70483,19 +71321,25 @@ function entryForRef(ref) {
|
|
|
70483
71321
|
}
|
|
70484
71322
|
if (ref.kind === "url") {
|
|
70485
71323
|
const id = `llm-custom-${createHash("sha1").update(ref.url).digest("hex").slice(0, 12)}`;
|
|
71324
|
+
const extraFiles = (ref.extraFiles ?? []).map((f) => ({
|
|
71325
|
+
url: f.url,
|
|
71326
|
+
filename: f.filename,
|
|
71327
|
+
sizeMB: mb(f.sizeBytes)
|
|
71328
|
+
}));
|
|
70486
71329
|
return { entry: {
|
|
70487
71330
|
id,
|
|
70488
|
-
name: id,
|
|
71331
|
+
name: ref.label ?? id,
|
|
70489
71332
|
description: "custom GGUF",
|
|
70490
71333
|
formats: { gguf: {
|
|
70491
71334
|
url: ref.url,
|
|
70492
|
-
sizeMB: 0
|
|
71335
|
+
sizeMB: ref.sizeBytes === void 0 ? 0 : mb(ref.sizeBytes)
|
|
70493
71336
|
} },
|
|
70494
71337
|
inputSize: {
|
|
70495
71338
|
width: 0,
|
|
70496
71339
|
height: 0
|
|
70497
71340
|
},
|
|
70498
|
-
labels: []
|
|
71341
|
+
labels: [],
|
|
71342
|
+
...extraFiles.length > 0 ? { extraFiles } : {}
|
|
70499
71343
|
} };
|
|
70500
71344
|
}
|
|
70501
71345
|
const id = `llm-path-${createHash("sha1").update(ref.path).digest("hex").slice(0, 12)}`;
|
|
@@ -70533,10 +71377,6 @@ function isNonEmptyFile(filePath) {
|
|
|
70533
71377
|
function siblingFilesFor(formatEntry) {
|
|
70534
71378
|
return formatEntry.isDirectory ? [] : formatEntry.files ?? [];
|
|
70535
71379
|
}
|
|
70536
|
-
/** Resolve a sibling's remote URL relative to the main file's directory. */
|
|
70537
|
-
function siblingUrl(mainUrl, sibling) {
|
|
70538
|
-
return mainUrl.replace(/[^/]+$/, sibling);
|
|
70539
|
-
}
|
|
70540
71380
|
/** Build fetch headers, including HF auth token for huggingface.co URLs */
|
|
70541
71381
|
function buildHeaders(url) {
|
|
70542
71382
|
const headers = { "User-Agent": "CamStack/1.0" };
|
|
@@ -70589,77 +71429,6 @@ async function downloadFile(url, destPath, onProgress) {
|
|
|
70589
71429
|
throw err;
|
|
70590
71430
|
}
|
|
70591
71431
|
}
|
|
70592
|
-
/**
|
|
70593
|
-
* Download every file in a HuggingFace directory bundle (e.g.,
|
|
70594
|
-
* `.mlpackage` / OpenVINO IR pair) atomically. `knownFiles` lists the
|
|
70595
|
-
* relative paths inside the directory; the function fetches each from
|
|
70596
|
-
* `${url}/${file}` and renames the staging directory only on full
|
|
70597
|
-
* success. Mirrors `ModelDownloadService.downloadDirectory` but
|
|
70598
|
-
* exposed as a standalone for catalog-less callers.
|
|
70599
|
-
*/
|
|
70600
|
-
async function downloadDirectory(url, destDir, knownFiles, onProgress) {
|
|
70601
|
-
const match = url.match(/huggingface\.co\/([^/]+\/[^/]+)\/resolve\/main\/(.+)/);
|
|
70602
|
-
if (!match) throw new Error(`Cannot parse HuggingFace URL: ${url}`);
|
|
70603
|
-
const [, repo, dirPath] = match;
|
|
70604
|
-
const files = (knownFiles ?? []).map((f) => ({
|
|
70605
|
-
relativePath: f,
|
|
70606
|
-
fileUrl: `https://huggingface.co/${repo}/resolve/main/${dirPath}/${f}`
|
|
70607
|
-
}));
|
|
70608
|
-
if (files.length === 0) throw new Error(`Directory bundle requires explicit \`files\` list (got none for ${url})`);
|
|
70609
|
-
const tmpDir = destDir + ".downloading";
|
|
70610
|
-
fs.rmSync(tmpDir, {
|
|
70611
|
-
recursive: true,
|
|
70612
|
-
force: true
|
|
70613
|
-
});
|
|
70614
|
-
fs.mkdirSync(tmpDir, { recursive: true });
|
|
70615
|
-
let totalDownloaded = 0;
|
|
70616
|
-
try {
|
|
70617
|
-
for (const file of files) {
|
|
70618
|
-
const destPath = path$1.join(tmpDir, file.relativePath);
|
|
70619
|
-
fs.mkdirSync(path$1.dirname(destPath), { recursive: true });
|
|
70620
|
-
await downloadFile(file.fileUrl, destPath, (downloaded, _total) => {
|
|
70621
|
-
onProgress?.(totalDownloaded + downloaded, void 0);
|
|
70622
|
-
});
|
|
70623
|
-
totalDownloaded += fs.statSync(destPath).size;
|
|
70624
|
-
}
|
|
70625
|
-
fs.rmSync(destDir, {
|
|
70626
|
-
recursive: true,
|
|
70627
|
-
force: true
|
|
70628
|
-
});
|
|
70629
|
-
fs.renameSync(tmpDir, destDir);
|
|
70630
|
-
} catch (err) {
|
|
70631
|
-
fs.rmSync(tmpDir, {
|
|
70632
|
-
recursive: true,
|
|
70633
|
-
force: true
|
|
70634
|
-
});
|
|
70635
|
-
throw err;
|
|
70636
|
-
}
|
|
70637
|
-
}
|
|
70638
|
-
/**
|
|
70639
|
-
* Resolve a `ModelCatalogEntry` against `modelsDir`: download model file
|
|
70640
|
-
* (or directory bundle) + extra files (labels JSON, charset dict, …),
|
|
70641
|
-
* skip if already on disk. Returns the local model path.
|
|
70642
|
-
*/
|
|
70643
|
-
async function ensureModel(modelsDir, entry, format, onProgress) {
|
|
70644
|
-
const formatEntry = entry.formats[format];
|
|
70645
|
-
if (!formatEntry) throw new Error(`Model "${entry.id}" has no ${format} format. Available: ${Object.keys(entry.formats).join(", ")}`);
|
|
70646
|
-
if (entry.extraFiles) for (const extra of entry.extraFiles) await downloadFile(extra.url, path$1.join(modelsDir, extra.filename));
|
|
70647
|
-
const filename = formatEntry.url.split("/").pop() ?? `${entry.id}.${format}`;
|
|
70648
|
-
const modelPath = path$1.join(modelsDir, filename);
|
|
70649
|
-
const siblings = siblingFilesFor(formatEntry);
|
|
70650
|
-
if (fs.existsSync(modelPath)) if (formatEntry.isDirectory && !fs.existsSync(path$1.join(modelPath, "Manifest.json"))) fs.rmSync(modelPath, {
|
|
70651
|
-
recursive: true,
|
|
70652
|
-
force: true
|
|
70653
|
-
});
|
|
70654
|
-
else if (siblings.some((f) => !isNonEmptyFile(path$1.join(modelsDir, f)))) {} else return modelPath;
|
|
70655
|
-
fs.mkdirSync(modelsDir, { recursive: true });
|
|
70656
|
-
if (formatEntry.isDirectory) await downloadDirectory(formatEntry.url, modelPath, formatEntry.files, onProgress);
|
|
70657
|
-
else {
|
|
70658
|
-
await downloadFile(formatEntry.url, modelPath, (downloaded, total) => onProgress?.(downloaded, total === 0 ? void 0 : total));
|
|
70659
|
-
for (const sibling of siblings) await downloadFile(siblingUrl(formatEntry.url, sibling), path$1.join(modelsDir, sibling));
|
|
70660
|
-
}
|
|
70661
|
-
return modelPath;
|
|
70662
|
-
}
|
|
70663
71432
|
/** Compute the on-disk path for a given model + format, even when not yet downloaded. */
|
|
70664
71433
|
function getModelFilePath(modelsDir, entry, format) {
|
|
70665
71434
|
const formatEntry = entry.formats[format];
|
|
@@ -70703,13 +71472,79 @@ promisify(gzip);
|
|
|
70703
71472
|
* Default `RuntimeModelOps` — the ONLY place the reused object-detection model
|
|
70704
71473
|
* mechanism is imported (the documented `@camstack/system/addon-utils`
|
|
70705
71474
|
* build-time-dep waiver that addon-post-analysis/addon-pipeline already use).
|
|
70706
|
-
* GGUFs ride `
|
|
70707
|
-
*
|
|
71475
|
+
* GGUFs ride the SHARED `downloadFile` (atomic `.downloading` + rename, HF
|
|
71476
|
+
* token headers, redirect following) — no bespoke fetcher (spec §4.2).
|
|
71477
|
+
*
|
|
71478
|
+
* ## Why this drives the file loop instead of calling `ensureModel`
|
|
71479
|
+
*
|
|
71480
|
+
* `ensureModel` downloads `extraFiles` FIRST and passes them NO progress
|
|
71481
|
+
* callback. That is invisible for a 40 kB labels JSON and unacceptable here: a
|
|
71482
|
+
* GGUF install is a 22 GB main file, up to N shards, and a 0.9 GB mmproj, and
|
|
71483
|
+
* under `ensureModel` every byte outside the main file moves in silence. A
|
|
71484
|
+
* multi-GB download that reports nothing reads as a hung node — the repo rule
|
|
71485
|
+
* is that a branch doing real work says so.
|
|
71486
|
+
*
|
|
71487
|
+
* So the loop is here, over the SAME `downloadFile`. What is gained: bytes
|
|
71488
|
+
* aggregated across the whole install, the name of the file currently moving,
|
|
71489
|
+
* and files already on disk excluded from the total rather than counted as
|
|
71490
|
+
* instantly-complete.
|
|
70708
71491
|
*/
|
|
70709
71492
|
var GGUF = "gguf";
|
|
71493
|
+
var BYTES_PER_MB = 1024 * 1024;
|
|
71494
|
+
/**
|
|
71495
|
+
* Main file first, then shards/mmproj. Deliberate: a gated or mistyped URL
|
|
71496
|
+
* fails on the file that matters before 0.9 GB of projector is spent on it.
|
|
71497
|
+
*/
|
|
71498
|
+
function planFiles(modelsDir, entry) {
|
|
71499
|
+
const out = [];
|
|
71500
|
+
const formatEntry = entry.formats[GGUF];
|
|
71501
|
+
if (formatEntry !== void 0) {
|
|
71502
|
+
const filename = formatEntry.url.split("/").pop() ?? `${entry.id}.${GGUF}`;
|
|
71503
|
+
out.push({
|
|
71504
|
+
url: formatEntry.url,
|
|
71505
|
+
destPath: path$1.join(modelsDir, filename),
|
|
71506
|
+
filename,
|
|
71507
|
+
expectedBytes: formatEntry.sizeMB * BYTES_PER_MB
|
|
71508
|
+
});
|
|
71509
|
+
}
|
|
71510
|
+
for (const extra of entry.extraFiles ?? []) out.push({
|
|
71511
|
+
url: extra.url,
|
|
71512
|
+
destPath: path$1.join(modelsDir, extra.filename),
|
|
71513
|
+
filename: extra.filename,
|
|
71514
|
+
expectedBytes: extra.sizeMB * BYTES_PER_MB
|
|
71515
|
+
});
|
|
71516
|
+
return out;
|
|
71517
|
+
}
|
|
70710
71518
|
function createDefaultModelOps(modelsDir) {
|
|
70711
71519
|
return {
|
|
70712
|
-
ensure: (entry, onProgress) =>
|
|
71520
|
+
ensure: async (entry, onProgress) => {
|
|
71521
|
+
if (entry.formats[GGUF] === void 0) throw new Error(`model ${entry.id} declares no gguf format`);
|
|
71522
|
+
const missing = planFiles(modelsDir, entry).filter((f) => !existsSync(f.destPath));
|
|
71523
|
+
const totalBytes = missing.reduce((sum, f) => sum + f.expectedBytes, 0);
|
|
71524
|
+
let carried = 0;
|
|
71525
|
+
for (const [index, file] of missing.entries()) {
|
|
71526
|
+
onProgress({
|
|
71527
|
+
file: file.filename,
|
|
71528
|
+
fileIndex: index + 1,
|
|
71529
|
+
fileCount: missing.length,
|
|
71530
|
+
downloadedBytes: carried,
|
|
71531
|
+
...totalBytes > 0 ? { totalBytes } : {}
|
|
71532
|
+
});
|
|
71533
|
+
await downloadFile(file.url, file.destPath, (downloaded) => {
|
|
71534
|
+
onProgress({
|
|
71535
|
+
file: file.filename,
|
|
71536
|
+
fileIndex: index + 1,
|
|
71537
|
+
fileCount: missing.length,
|
|
71538
|
+
downloadedBytes: carried + downloaded,
|
|
71539
|
+
...totalBytes > 0 ? { totalBytes } : {}
|
|
71540
|
+
});
|
|
71541
|
+
});
|
|
71542
|
+
carried += existsSync(file.destPath) ? statSync(file.destPath).size : file.expectedBytes;
|
|
71543
|
+
}
|
|
71544
|
+
const main = getModelFilePath(modelsDir, entry, GGUF);
|
|
71545
|
+
if (main === null) throw new Error(`no gguf path for model ${entry.id}`);
|
|
71546
|
+
return main;
|
|
71547
|
+
},
|
|
70713
71548
|
isDownloaded: (entry) => isModelDownloaded(modelsDir, entry, GGUF),
|
|
70714
71549
|
pathFor: (entry) => {
|
|
70715
71550
|
const p = getModelFilePath(modelsDir, entry, GGUF);
|
|
@@ -70723,318 +71558,6 @@ function createDefaultModelOps(modelsDir) {
|
|
|
70723
71558
|
};
|
|
70724
71559
|
}
|
|
70725
71560
|
//#endregion
|
|
70726
|
-
//#region src/runtime/sha256.ts
|
|
70727
|
-
/**
|
|
70728
|
-
* File sha256 — a local copy of the private `computeSha256` at
|
|
70729
|
-
* model-downloader.ts (not exported from @camstack/system), streamed so it
|
|
70730
|
-
* never buffers a multi-GB artifact.
|
|
70731
|
-
*/
|
|
70732
|
-
function fileSha256(filePath) {
|
|
70733
|
-
return new Promise((resolve, reject) => {
|
|
70734
|
-
const hash = createHash("sha256");
|
|
70735
|
-
const stream = createReadStream(filePath);
|
|
70736
|
-
stream.on("error", reject);
|
|
70737
|
-
stream.on("data", (chunk) => hash.update(chunk));
|
|
70738
|
-
stream.on("end", () => resolve(hash.digest("hex")));
|
|
70739
|
-
});
|
|
70740
|
-
}
|
|
70741
|
-
//#endregion
|
|
70742
|
-
//#region src/runtime/runtime-provider.ts
|
|
70743
|
-
/**
|
|
70744
|
-
* `llm-runtime` provider — the node-side managed executor. Reuses the
|
|
70745
|
-
* object-detection model plane (ensureModel/isModelDownloaded/delete via the
|
|
70746
|
-
* injected `RuntimeModelOps`) for GGUF artifacts, the `LlamaSupervisor` for the
|
|
70747
|
-
* llama-server child, and the SHARED {@link LlmClient} for the local inference
|
|
70748
|
-
* wire (only lifecycle + locality differ — spec §2). GGUFs are
|
|
70749
|
-
* multi-GB, so a missing model is an EXPLICIT-install error, never an auto-pull.
|
|
70750
|
-
* Usage rows are written hub-side only (single accounting point).
|
|
70751
|
-
*/
|
|
70752
|
-
function basename(url) {
|
|
70753
|
-
const clean = url.split("?")[0] ?? url;
|
|
70754
|
-
return clean.slice(clean.lastIndexOf("/") + 1);
|
|
70755
|
-
}
|
|
70756
|
-
function catalogIdForFile(file) {
|
|
70757
|
-
return LLM_MODEL_CATALOG.find((m) => basename(m.meta.url) === file)?.meta.id;
|
|
70758
|
-
}
|
|
70759
|
-
function mmprojFilename(entry) {
|
|
70760
|
-
return entry.extraFiles?.[0]?.filename;
|
|
70761
|
-
}
|
|
70762
|
-
function createLlmRuntimeProvider(deps) {
|
|
70763
|
-
let downloadProgress;
|
|
70764
|
-
async function resolvePaths(runtime) {
|
|
70765
|
-
const resolution = entryForRef(runtime.model);
|
|
70766
|
-
if (resolution === null) throw new Error("unknown model reference");
|
|
70767
|
-
const { entry, localPathOverride } = resolution;
|
|
70768
|
-
const modelPath = localPathOverride ?? deps.modelOps.pathFor(entry);
|
|
70769
|
-
const mmproj = mmprojFilename(entry);
|
|
70770
|
-
return {
|
|
70771
|
-
modelId: entry.id,
|
|
70772
|
-
modelPath,
|
|
70773
|
-
...mmproj !== void 0 ? { mmprojPath: deps.modelOps.extraFilePath(entry, mmproj) } : {}
|
|
70774
|
-
};
|
|
70775
|
-
}
|
|
70776
|
-
function installedGuard(runtime) {
|
|
70777
|
-
const resolution = entryForRef(runtime.model);
|
|
70778
|
-
if (resolution === null) return {
|
|
70779
|
-
ok: false,
|
|
70780
|
-
message: "unknown model reference"
|
|
70781
|
-
};
|
|
70782
|
-
if (resolution.localPathOverride !== void 0) return { ok: true };
|
|
70783
|
-
if (!deps.modelOps.isDownloaded(resolution.entry)) return {
|
|
70784
|
-
ok: false,
|
|
70785
|
-
message: `model ${resolution.entry.id} not installed on node ${deps.nodeId}`
|
|
70786
|
-
};
|
|
70787
|
-
return { ok: true };
|
|
70788
|
-
}
|
|
70789
|
-
async function ensureStartedInternal(runtime) {
|
|
70790
|
-
const binaryPath = await deps.ensureBinary();
|
|
70791
|
-
const paths = await resolvePaths(runtime);
|
|
70792
|
-
const startCfg = {
|
|
70793
|
-
nodeId: deps.nodeId,
|
|
70794
|
-
modelId: paths.modelId,
|
|
70795
|
-
modelPath: paths.modelPath,
|
|
70796
|
-
...paths.mmprojPath !== void 0 ? { mmprojPath: paths.mmprojPath } : {},
|
|
70797
|
-
contextSize: runtime.contextSize,
|
|
70798
|
-
gpuLayers: runtime.gpuLayers,
|
|
70799
|
-
...runtime.threads !== void 0 ? { threads: runtime.threads } : {},
|
|
70800
|
-
parallel: runtime.parallel,
|
|
70801
|
-
...runtime.batchSize !== void 0 ? { batchSize: runtime.batchSize } : {},
|
|
70802
|
-
...runtime.ubatchSize !== void 0 ? { ubatchSize: runtime.ubatchSize } : {},
|
|
70803
|
-
flashAttention: runtime.flashAttention,
|
|
70804
|
-
mlock: runtime.mlock,
|
|
70805
|
-
noMmap: runtime.noMmap,
|
|
70806
|
-
...runtime.cacheTypeK !== void 0 ? { cacheTypeK: runtime.cacheTypeK } : {},
|
|
70807
|
-
...runtime.cacheTypeV !== void 0 ? { cacheTypeV: runtime.cacheTypeV } : {},
|
|
70808
|
-
idleStopMinutes: runtime.idleStopMinutes,
|
|
70809
|
-
binaryPath
|
|
70810
|
-
};
|
|
70811
|
-
return deps.supervisor.start(startCfg);
|
|
70812
|
-
}
|
|
70813
|
-
function status() {
|
|
70814
|
-
return {
|
|
70815
|
-
...deps.supervisor.status(),
|
|
70816
|
-
nodeId: deps.nodeId,
|
|
70817
|
-
...downloadProgress !== void 0 ? { downloadProgress } : {}
|
|
70818
|
-
};
|
|
70819
|
-
}
|
|
70820
|
-
return {
|
|
70821
|
-
complete: async (input) => {
|
|
70822
|
-
const guard = installedGuard(input.runtime);
|
|
70823
|
-
if (!guard.ok) return {
|
|
70824
|
-
ok: false,
|
|
70825
|
-
code: "unavailable",
|
|
70826
|
-
message: guard.message
|
|
70827
|
-
};
|
|
70828
|
-
await ensureStartedInternal(input.runtime);
|
|
70829
|
-
const port = deps.supervisor.port;
|
|
70830
|
-
if (port === void 0) return {
|
|
70831
|
-
ok: false,
|
|
70832
|
-
code: "unavailable",
|
|
70833
|
-
message: "llama-server has no port"
|
|
70834
|
-
};
|
|
70835
|
-
const paths = await resolvePaths(input.runtime);
|
|
70836
|
-
const timeoutMs = input.timeoutMs ?? 12e4;
|
|
70837
|
-
const localProfile = {
|
|
70838
|
-
id: "managed-local",
|
|
70839
|
-
name: "managed-local",
|
|
70840
|
-
kind: "openai-compatible",
|
|
70841
|
-
addonId: "ai",
|
|
70842
|
-
enabled: true,
|
|
70843
|
-
model: paths.modelId,
|
|
70844
|
-
baseUrl: `http://127.0.0.1:${String(port)}/v1`,
|
|
70845
|
-
supportsVision: paths.mmprojPath !== void 0,
|
|
70846
|
-
timeoutMs,
|
|
70847
|
-
connectTimeoutMs: LlmTimeoutDefaults.connectMs,
|
|
70848
|
-
firstTokenTimeoutMs: LlmTimeoutDefaults.firstTokenMs,
|
|
70849
|
-
idleTimeoutMs: LlmTimeoutDefaults.idleMs,
|
|
70850
|
-
retry: {
|
|
70851
|
-
enabled: false,
|
|
70852
|
-
maxAttempts: 1
|
|
70853
|
-
},
|
|
70854
|
-
toolsEnabled: false
|
|
70855
|
-
};
|
|
70856
|
-
const result = await deps.client.generate({
|
|
70857
|
-
profile: localProfile,
|
|
70858
|
-
...input.system !== void 0 ? { system: input.system } : {},
|
|
70859
|
-
prompt: input.prompt,
|
|
70860
|
-
...input.images !== void 0 ? { images: input.images } : {},
|
|
70861
|
-
...input.jsonSchema !== void 0 ? { jsonSchema: input.jsonSchema } : {},
|
|
70862
|
-
...input.maxTokens !== void 0 ? { maxTokens: input.maxTokens } : {},
|
|
70863
|
-
...input.temperature !== void 0 ? { temperature: input.temperature } : {},
|
|
70864
|
-
...input.topP !== void 0 ? { topP: input.topP } : {},
|
|
70865
|
-
...input.topK !== void 0 ? { topK: input.topK } : {},
|
|
70866
|
-
signal: new AbortController().signal
|
|
70867
|
-
}, timeoutMs);
|
|
70868
|
-
deps.supervisor.noteActivity();
|
|
70869
|
-
return result;
|
|
70870
|
-
},
|
|
70871
|
-
ensureStarted: async ({ runtime }) => {
|
|
70872
|
-
const guard = installedGuard(runtime);
|
|
70873
|
-
if (!guard.ok) return {
|
|
70874
|
-
nodeId: deps.nodeId,
|
|
70875
|
-
state: "stopped",
|
|
70876
|
-
lastError: guard.message,
|
|
70877
|
-
crashesInWindow: 0
|
|
70878
|
-
};
|
|
70879
|
-
return ensureStartedInternal(runtime);
|
|
70880
|
-
},
|
|
70881
|
-
stop: async () => {
|
|
70882
|
-
await deps.supervisor.stop();
|
|
70883
|
-
},
|
|
70884
|
-
status: async () => status(),
|
|
70885
|
-
installModel: async ({ model }) => {
|
|
70886
|
-
const resolution = entryForRef(model);
|
|
70887
|
-
if (resolution === null) throw new Error("unknown model reference");
|
|
70888
|
-
if (resolution.localPathOverride !== void 0) return;
|
|
70889
|
-
downloadProgress = 0;
|
|
70890
|
-
try {
|
|
70891
|
-
await deps.modelOps.ensure(resolution.entry, (frac) => {
|
|
70892
|
-
downloadProgress = frac;
|
|
70893
|
-
});
|
|
70894
|
-
if (model.kind === "url" && model.sha256 !== void 0) {
|
|
70895
|
-
const filePath = deps.modelOps.pathFor(resolution.entry);
|
|
70896
|
-
if (await (deps.fileSha256 ?? fileSha256)(filePath) !== model.sha256) {
|
|
70897
|
-
await deps.modelOps.delete(resolution.entry);
|
|
70898
|
-
throw new Error(`sha256 mismatch for ${model.url}`);
|
|
70899
|
-
}
|
|
70900
|
-
}
|
|
70901
|
-
} finally {
|
|
70902
|
-
downloadProgress = void 0;
|
|
70903
|
-
}
|
|
70904
|
-
},
|
|
70905
|
-
deleteModel: async ({ file }) => {
|
|
70906
|
-
const loaded = deps.supervisor.status().modelPath;
|
|
70907
|
-
if (loaded !== void 0 && basename(loaded) === file) throw new Error(`cannot delete ${file}: loaded by the running runtime`);
|
|
70908
|
-
await fsp.rm(path$1.join(deps.modelsDir, file), { force: true });
|
|
70909
|
-
},
|
|
70910
|
-
listLocalModels: async () => {
|
|
70911
|
-
return (await listGgufFiles(deps.modelsDir)).map((f) => {
|
|
70912
|
-
const catalogId = catalogIdForFile(f.file);
|
|
70913
|
-
return {
|
|
70914
|
-
file: f.file,
|
|
70915
|
-
sizeBytes: f.sizeBytes,
|
|
70916
|
-
...catalogId !== void 0 ? { catalogId } : {}
|
|
70917
|
-
};
|
|
70918
|
-
});
|
|
70919
|
-
},
|
|
70920
|
-
getDiskUsage: async () => {
|
|
70921
|
-
const modelsBytes = (await listGgufFiles(deps.modelsDir)).reduce((sum, f) => sum + f.sizeBytes, 0);
|
|
70922
|
-
return {
|
|
70923
|
-
nodeId: deps.nodeId,
|
|
70924
|
-
modelsBytes
|
|
70925
|
-
};
|
|
70926
|
-
}
|
|
70927
|
-
};
|
|
70928
|
-
}
|
|
70929
|
-
async function listGgufFiles(dir) {
|
|
70930
|
-
let names;
|
|
70931
|
-
try {
|
|
70932
|
-
names = await fsp.readdir(dir);
|
|
70933
|
-
} catch {
|
|
70934
|
-
return [];
|
|
70935
|
-
}
|
|
70936
|
-
const out = [];
|
|
70937
|
-
for (const name of names) {
|
|
70938
|
-
if (!name.endsWith(".gguf")) continue;
|
|
70939
|
-
try {
|
|
70940
|
-
const stat = await fsp.stat(path$1.join(dir, name));
|
|
70941
|
-
out.push({
|
|
70942
|
-
file: name,
|
|
70943
|
-
sizeBytes: stat.size
|
|
70944
|
-
});
|
|
70945
|
-
} catch {}
|
|
70946
|
-
}
|
|
70947
|
-
return out;
|
|
70948
|
-
}
|
|
70949
|
-
//#endregion
|
|
70950
|
-
//#region src/runtime-client.ts
|
|
70951
|
-
/**
|
|
70952
|
-
* `RuntimeClient` over the cap plane — every verb pins the target node with
|
|
70953
|
-
* `nodePin(nodeId)` (transport-level, never a hand-rolled routing field). The
|
|
70954
|
-
* CapRouteResolver classifies the pin into hub-local-uds / remote-moleculer /
|
|
70955
|
-
* agent-child-forward transparently (spec §4.1; the model-studio cross-node
|
|
70956
|
-
* convert precedent). Node enumeration is the `nodes.topology` roster filtered
|
|
70957
|
-
* to nodes advertising the `llm-runtime` cap — never a shadow registry.
|
|
70958
|
-
*/
|
|
70959
|
-
var LLM_RUNTIME_CAP = "llm-runtime";
|
|
70960
|
-
function createRuntimeClient(api) {
|
|
70961
|
-
return {
|
|
70962
|
-
complete: (nodeId, input) => api.llmRuntime.complete.mutate(input, nodePin(nodeId)),
|
|
70963
|
-
ensureStarted: (nodeId, runtime) => api.llmRuntime.ensureStarted.mutate({ runtime }, nodePin(nodeId)),
|
|
70964
|
-
stopRuntime: (nodeId) => api.llmRuntime.stop.mutate({}, nodePin(nodeId)),
|
|
70965
|
-
status: (nodeId) => api.llmRuntime.status.query({}, nodePin(nodeId)),
|
|
70966
|
-
installModel: (nodeId, model) => api.llmRuntime.installModel.mutate({ model }, nodePin(nodeId)),
|
|
70967
|
-
deleteModel: (nodeId, file) => api.llmRuntime.deleteModel.mutate({ file }, nodePin(nodeId)),
|
|
70968
|
-
listLocalModels: (nodeId) => api.llmRuntime.listLocalModels.query({}, nodePin(nodeId)),
|
|
70969
|
-
getDiskUsage: (nodeId) => api.llmRuntime.getDiskUsage.query({}, nodePin(nodeId)),
|
|
70970
|
-
listRuntimeNodeIds: async () => {
|
|
70971
|
-
const topology = await api.nodes.topology.query();
|
|
70972
|
-
const ids = /* @__PURE__ */ new Set();
|
|
70973
|
-
for (const node of topology) if (node.addons.some((a) => a.capabilities.includes(LLM_RUNTIME_CAP))) ids.add(node.id);
|
|
70974
|
-
return [...ids];
|
|
70975
|
-
}
|
|
70976
|
-
};
|
|
70977
|
-
}
|
|
70978
|
-
//#endregion
|
|
70979
|
-
//#region src/assembly.ts
|
|
70980
|
-
/**
|
|
70981
|
-
* Registration assembly (the hub/agent split, spec §1). Every node running
|
|
70982
|
-
* addon-ai registers `llm-runtime`; ONLY the hub also registers the consumer
|
|
70983
|
-
* `llm` surface (profiles/usage need outbound internet + API keys).
|
|
70984
|
-
* Extracted from the addon class so the split + seeding is unit-testable
|
|
70985
|
-
* without a full AddonContext.
|
|
70986
|
-
*/
|
|
70987
|
-
async function assembleAi(deps) {
|
|
70988
|
-
const client = deps.client ?? createLlmClient();
|
|
70989
|
-
const runtimeProvider = createLlmRuntimeProvider({
|
|
70990
|
-
nodeId: deps.nodeId,
|
|
70991
|
-
modelsDir: deps.modelsDir,
|
|
70992
|
-
ensureBinary: deps.ensureBinary,
|
|
70993
|
-
supervisor: deps.supervisor,
|
|
70994
|
-
modelOps: createDefaultModelOps(deps.modelsDir),
|
|
70995
|
-
client,
|
|
70996
|
-
logger: deps.logger.child("llm-runtime")
|
|
70997
|
-
});
|
|
70998
|
-
const registrations = [{
|
|
70999
|
-
capability: llmRuntimeCapability,
|
|
71000
|
-
provider: runtimeProvider
|
|
71001
|
-
}];
|
|
71002
|
-
if (!deps.isHub) return {
|
|
71003
|
-
registrations,
|
|
71004
|
-
runtimeProvider
|
|
71005
|
-
};
|
|
71006
|
-
const { UsageStore } = await import("./usage-store-RiVXP_ma.mjs").then((n) => n.i);
|
|
71007
|
-
const store = new ProfileStore(deps.settingsPort);
|
|
71008
|
-
const defaults = new DefaultsStore(deps.settingsPort);
|
|
71009
|
-
const usage = new UsageStore(deps.settingsPort, Date.now, deps.logger.child("llm-usage"));
|
|
71010
|
-
await store.init();
|
|
71011
|
-
await defaults.init();
|
|
71012
|
-
await usage.init();
|
|
71013
|
-
await store.ensureSeeded();
|
|
71014
|
-
const llmProvider = createLlmProvider({
|
|
71015
|
-
store,
|
|
71016
|
-
defaults,
|
|
71017
|
-
usage,
|
|
71018
|
-
client,
|
|
71019
|
-
...deps.runtimeApi !== void 0 ? { runtime: createRuntimeClient(deps.runtimeApi) } : {},
|
|
71020
|
-
...deps.distributeModel !== void 0 ? { distributeModel: deps.distributeModel } : {},
|
|
71021
|
-
catalog: LLM_MODEL_CATALOG.map((m) => m.meta),
|
|
71022
|
-
logger: deps.logger.child("llm")
|
|
71023
|
-
});
|
|
71024
|
-
registrations.push({
|
|
71025
|
-
capability: llmCapability,
|
|
71026
|
-
provider: llmProvider
|
|
71027
|
-
});
|
|
71028
|
-
return {
|
|
71029
|
-
registrations,
|
|
71030
|
-
runtimeProvider,
|
|
71031
|
-
llmProvider,
|
|
71032
|
-
store,
|
|
71033
|
-
usage,
|
|
71034
|
-
prune: (retentionDays) => usage.prune(retentionDays)
|
|
71035
|
-
};
|
|
71036
|
-
}
|
|
71037
|
-
//#endregion
|
|
71038
71561
|
//#region src/runtime/crash-policy.ts
|
|
71039
71562
|
var CrashPolicy = class {
|
|
71040
71563
|
opts;
|
|
@@ -71084,6 +71607,63 @@ var DEFAULT_CRASH_POLICY = {
|
|
|
71084
71607
|
* v1: at most one running child. Resource ceiling = llama-server flags +
|
|
71085
71608
|
* idleStopMinutes ONLY (no RSS watchdog — operator decision #3).
|
|
71086
71609
|
*/
|
|
71610
|
+
/**
|
|
71611
|
+
* Every llama-server flag a TYPED field above already owns, mapped to the
|
|
71612
|
+
* field that owns it.
|
|
71613
|
+
*
|
|
71614
|
+
* This map is the whole reconciliation between the typed tuning surface and
|
|
71615
|
+
* the free-text "additional arguments" box. Both exist because neither is
|
|
71616
|
+
* sufficient — the typed fields give the common knobs a validated control and
|
|
71617
|
+
* a default, and llama.cpp has a hundred flags nobody is going to model — but
|
|
71618
|
+
* a flag settable from BOTH is a bug generator: whichever one loses is a
|
|
71619
|
+
* control the operator watched do nothing. So the box is an escape hatch for
|
|
71620
|
+
* what is NOT modelled, and reaching into it for something that is gets
|
|
71621
|
+
* rejected by name.
|
|
71622
|
+
*/
|
|
71623
|
+
var OWNED_FLAGS = {
|
|
71624
|
+
"-m": "model",
|
|
71625
|
+
"--model": "model",
|
|
71626
|
+
"--host": "fixed to 127.0.0.1",
|
|
71627
|
+
"--port": "assigned by the supervisor",
|
|
71628
|
+
"-c": "contextSize",
|
|
71629
|
+
"--ctx-size": "contextSize",
|
|
71630
|
+
"-ngl": "gpuLayers",
|
|
71631
|
+
"--gpu-layers": "gpuLayers",
|
|
71632
|
+
"--n-gpu-layers": "gpuLayers",
|
|
71633
|
+
"-t": "threads",
|
|
71634
|
+
"--threads": "threads",
|
|
71635
|
+
"--parallel": "parallel",
|
|
71636
|
+
"-np": "parallel",
|
|
71637
|
+
"-b": "batchSize",
|
|
71638
|
+
"--batch-size": "batchSize",
|
|
71639
|
+
"-ub": "ubatchSize",
|
|
71640
|
+
"--ubatch-size": "ubatchSize",
|
|
71641
|
+
"-fa": "flashAttention",
|
|
71642
|
+
"--flash-attn": "flashAttention",
|
|
71643
|
+
"--mlock": "mlock",
|
|
71644
|
+
"--no-mmap": "noMmap",
|
|
71645
|
+
"-ctk": "cacheTypeK",
|
|
71646
|
+
"--cache-type-k": "cacheTypeK",
|
|
71647
|
+
"-ctv": "cacheTypeV",
|
|
71648
|
+
"--cache-type-v": "cacheTypeV",
|
|
71649
|
+
"--mmproj": "the vision model’s projector"
|
|
71650
|
+
};
|
|
71651
|
+
/**
|
|
71652
|
+
* Reject an `extraArgs` list that reaches for a flag a typed field owns.
|
|
71653
|
+
* `--flag=value` counts as `--flag`.
|
|
71654
|
+
*/
|
|
71655
|
+
function checkExtraArgs(extraArgs) {
|
|
71656
|
+
for (const token of extraArgs) {
|
|
71657
|
+
if (!token.startsWith("-")) continue;
|
|
71658
|
+
const flag = token.split("=")[0] ?? token;
|
|
71659
|
+
const owner = OWNED_FLAGS[flag];
|
|
71660
|
+
if (owner !== void 0) return {
|
|
71661
|
+
ok: false,
|
|
71662
|
+
message: `"${flag}" is already set by the runtime field "${owner}" — set it there, not in additional arguments (a flag with two owners is a control that silently does nothing)`
|
|
71663
|
+
};
|
|
71664
|
+
}
|
|
71665
|
+
return { ok: true };
|
|
71666
|
+
}
|
|
71087
71667
|
var HEALTH_GATE_INTERVAL_MS = 500;
|
|
71088
71668
|
function defaultPickPort() {
|
|
71089
71669
|
return new Promise((resolve, reject) => {
|
|
@@ -71134,6 +71714,7 @@ function buildLlamaArgs(cfg, port) {
|
|
|
71134
71714
|
if (cfg.cacheTypeK !== void 0) args.push("--cache-type-k", cfg.cacheTypeK);
|
|
71135
71715
|
if (cfg.cacheTypeV !== void 0) args.push("--cache-type-v", cfg.cacheTypeV);
|
|
71136
71716
|
if (cfg.mmprojPath !== void 0) args.push("--mmproj", cfg.mmprojPath);
|
|
71717
|
+
args.push(...cfg.extraArgs ?? []);
|
|
71137
71718
|
return args;
|
|
71138
71719
|
}
|
|
71139
71720
|
var LlamaSupervisor = class {
|
|
@@ -71358,6 +71939,463 @@ var LlamaSupervisor = class {
|
|
|
71358
71939
|
}
|
|
71359
71940
|
};
|
|
71360
71941
|
//#endregion
|
|
71942
|
+
//#region src/runtime/sha256.ts
|
|
71943
|
+
/**
|
|
71944
|
+
* File sha256 — a local copy of the private `computeSha256` at
|
|
71945
|
+
* model-downloader.ts (not exported from @camstack/system), streamed so it
|
|
71946
|
+
* never buffers a multi-GB artifact.
|
|
71947
|
+
*/
|
|
71948
|
+
function fileSha256(filePath) {
|
|
71949
|
+
return new Promise((resolve, reject) => {
|
|
71950
|
+
const hash = createHash("sha256");
|
|
71951
|
+
const stream = createReadStream(filePath);
|
|
71952
|
+
stream.on("error", reject);
|
|
71953
|
+
stream.on("data", (chunk) => hash.update(chunk));
|
|
71954
|
+
stream.on("end", () => resolve(hash.digest("hex")));
|
|
71955
|
+
});
|
|
71956
|
+
}
|
|
71957
|
+
//#endregion
|
|
71958
|
+
//#region src/runtime/runtime-provider.ts
|
|
71959
|
+
/**
|
|
71960
|
+
* `llm-runtime` provider — the node-side managed executor. Reuses the
|
|
71961
|
+
* object-detection model plane (ensureModel/isModelDownloaded/delete via the
|
|
71962
|
+
* injected `RuntimeModelOps`) for GGUF artifacts, the `LlamaSupervisor` for the
|
|
71963
|
+
* llama-server child, and the SHARED {@link LlmClient} for the local inference
|
|
71964
|
+
* wire (only lifecycle + locality differ — spec §2). GGUFs are
|
|
71965
|
+
* multi-GB, so a missing model is an EXPLICIT-install error, never an auto-pull.
|
|
71966
|
+
* Usage rows are written hub-side only (single accounting point).
|
|
71967
|
+
*/
|
|
71968
|
+
function basename(url) {
|
|
71969
|
+
const clean = url.split("?")[0] ?? url;
|
|
71970
|
+
return clean.slice(clean.lastIndexOf("/") + 1);
|
|
71971
|
+
}
|
|
71972
|
+
function catalogIdForFile(file) {
|
|
71973
|
+
return LLM_MODEL_CATALOG.find((m) => basename(m.meta.url) === file)?.meta.id;
|
|
71974
|
+
}
|
|
71975
|
+
/**
|
|
71976
|
+
* The projector among the extra files — matched by NAME, not by position.
|
|
71977
|
+
*
|
|
71978
|
+
* `extraFiles[0]` was safe while the only extra a GGUF entry ever had was an
|
|
71979
|
+
* mmproj. A split GGUF puts shards 2..N in the same list, so index 0 is now
|
|
71980
|
+
* routinely a weights shard, and passing one to `--mmproj` starts llama-server
|
|
71981
|
+
* against a file that is not a projector.
|
|
71982
|
+
*/
|
|
71983
|
+
function mmprojFilename(entry) {
|
|
71984
|
+
return entry.extraFiles?.find((f) => f.filename.toLowerCase().startsWith("mmproj"))?.filename;
|
|
71985
|
+
}
|
|
71986
|
+
function gb(bytes) {
|
|
71987
|
+
return `${(bytes / 1e9).toFixed(2)} GB`;
|
|
71988
|
+
}
|
|
71989
|
+
function createLlmRuntimeProvider(deps) {
|
|
71990
|
+
let downloadProgress;
|
|
71991
|
+
let download;
|
|
71992
|
+
async function resolvePaths(runtime) {
|
|
71993
|
+
const resolution = entryForRef(runtime.model);
|
|
71994
|
+
if (resolution === null) throw new Error("unknown model reference");
|
|
71995
|
+
const { entry, localPathOverride } = resolution;
|
|
71996
|
+
const modelPath = localPathOverride ?? deps.modelOps.pathFor(entry);
|
|
71997
|
+
const mmproj = mmprojFilename(entry);
|
|
71998
|
+
return {
|
|
71999
|
+
modelId: entry.id,
|
|
72000
|
+
modelPath,
|
|
72001
|
+
...mmproj !== void 0 ? { mmprojPath: deps.modelOps.extraFilePath(entry, mmproj) } : {}
|
|
72002
|
+
};
|
|
72003
|
+
}
|
|
72004
|
+
function installedGuard(runtime) {
|
|
72005
|
+
const resolution = entryForRef(runtime.model);
|
|
72006
|
+
if (resolution === null) return {
|
|
72007
|
+
ok: false,
|
|
72008
|
+
message: "unknown model reference"
|
|
72009
|
+
};
|
|
72010
|
+
if (resolution.localPathOverride !== void 0) return { ok: true };
|
|
72011
|
+
if (!deps.modelOps.isDownloaded(resolution.entry)) return {
|
|
72012
|
+
ok: false,
|
|
72013
|
+
message: `model ${resolution.entry.id} not installed on node ${deps.nodeId}`
|
|
72014
|
+
};
|
|
72015
|
+
return { ok: true };
|
|
72016
|
+
}
|
|
72017
|
+
async function ensureStartedInternal(runtime) {
|
|
72018
|
+
const argCheck = checkExtraArgs(runtime.extraArgs);
|
|
72019
|
+
if (!argCheck.ok) throw new Error(argCheck.message);
|
|
72020
|
+
const binaryPath = await deps.ensureBinary();
|
|
72021
|
+
const paths = await resolvePaths(runtime);
|
|
72022
|
+
const startCfg = {
|
|
72023
|
+
nodeId: deps.nodeId,
|
|
72024
|
+
modelId: paths.modelId,
|
|
72025
|
+
modelPath: paths.modelPath,
|
|
72026
|
+
...paths.mmprojPath !== void 0 ? { mmprojPath: paths.mmprojPath } : {},
|
|
72027
|
+
contextSize: runtime.contextSize,
|
|
72028
|
+
gpuLayers: runtime.gpuLayers,
|
|
72029
|
+
...runtime.threads !== void 0 ? { threads: runtime.threads } : {},
|
|
72030
|
+
parallel: runtime.parallel,
|
|
72031
|
+
...runtime.batchSize !== void 0 ? { batchSize: runtime.batchSize } : {},
|
|
72032
|
+
...runtime.ubatchSize !== void 0 ? { ubatchSize: runtime.ubatchSize } : {},
|
|
72033
|
+
flashAttention: runtime.flashAttention,
|
|
72034
|
+
mlock: runtime.mlock,
|
|
72035
|
+
noMmap: runtime.noMmap,
|
|
72036
|
+
...runtime.cacheTypeK !== void 0 ? { cacheTypeK: runtime.cacheTypeK } : {},
|
|
72037
|
+
...runtime.cacheTypeV !== void 0 ? { cacheTypeV: runtime.cacheTypeV } : {},
|
|
72038
|
+
extraArgs: runtime.extraArgs,
|
|
72039
|
+
idleStopMinutes: runtime.idleStopMinutes,
|
|
72040
|
+
binaryPath
|
|
72041
|
+
};
|
|
72042
|
+
return deps.supervisor.start(startCfg);
|
|
72043
|
+
}
|
|
72044
|
+
/**
|
|
72045
|
+
* sha256 every artifact whose digest the reference pinned — the main file
|
|
72046
|
+
* AND the extras.
|
|
72047
|
+
*
|
|
72048
|
+
* Verifying only the main file was the gap: a truncated or swapped mmproj is
|
|
72049
|
+
* exactly as fatal to llama-server as a bad weights file, and a resolved HF
|
|
72050
|
+
* reference carries a digest for every artifact (LFS `oid`) so there is no
|
|
72051
|
+
* reason to check one and trust the rest.
|
|
72052
|
+
*
|
|
72053
|
+
* This pass reads tens of GB and takes minutes; it is a REPORTED phase, not
|
|
72054
|
+
* a silent tail, because a progress bar frozen at 100% is the shape of a
|
|
72055
|
+
* hang.
|
|
72056
|
+
*/
|
|
72057
|
+
async function verifyDigests(entry, model, startedAt) {
|
|
72058
|
+
if (model.kind !== "url") return;
|
|
72059
|
+
const targets = [];
|
|
72060
|
+
if (model.sha256 !== void 0) targets.push({
|
|
72061
|
+
filePath: deps.modelOps.pathFor(entry),
|
|
72062
|
+
sha256: model.sha256,
|
|
72063
|
+
name: basename(model.url)
|
|
72064
|
+
});
|
|
72065
|
+
for (const extra of model.extraFiles ?? []) {
|
|
72066
|
+
if (extra.sha256 === void 0) continue;
|
|
72067
|
+
targets.push({
|
|
72068
|
+
filePath: deps.modelOps.extraFilePath(entry, extra.filename),
|
|
72069
|
+
sha256: extra.sha256,
|
|
72070
|
+
name: extra.filename
|
|
72071
|
+
});
|
|
72072
|
+
}
|
|
72073
|
+
if (targets.length === 0) return;
|
|
72074
|
+
const sha256 = deps.fileSha256 ?? fileSha256;
|
|
72075
|
+
for (const [index, target] of targets.entries()) {
|
|
72076
|
+
download = {
|
|
72077
|
+
phase: "verifying",
|
|
72078
|
+
file: target.name,
|
|
72079
|
+
fileIndex: index + 1,
|
|
72080
|
+
fileCount: targets.length,
|
|
72081
|
+
downloadedBytes: 0
|
|
72082
|
+
};
|
|
72083
|
+
deps.logger.info("llm model verifying digest", { meta: {
|
|
72084
|
+
nodeId: deps.nodeId,
|
|
72085
|
+
modelId: entry.id,
|
|
72086
|
+
file: target.name
|
|
72087
|
+
} });
|
|
72088
|
+
const digest = await sha256(target.filePath);
|
|
72089
|
+
if (digest !== target.sha256) {
|
|
72090
|
+
deps.logger.error("llm model digest mismatch; discarding the download", { meta: {
|
|
72091
|
+
nodeId: deps.nodeId,
|
|
72092
|
+
modelId: entry.id,
|
|
72093
|
+
file: target.name,
|
|
72094
|
+
expected: target.sha256,
|
|
72095
|
+
actual: digest,
|
|
72096
|
+
elapsedMs: Date.now() - startedAt
|
|
72097
|
+
} });
|
|
72098
|
+
await deps.modelOps.delete(entry);
|
|
72099
|
+
await fsp.rm(target.filePath, { force: true });
|
|
72100
|
+
throw new Error(`sha256 mismatch for ${target.name}: expected ${target.sha256}, got ${digest}`);
|
|
72101
|
+
}
|
|
72102
|
+
}
|
|
72103
|
+
}
|
|
72104
|
+
function status() {
|
|
72105
|
+
return {
|
|
72106
|
+
...deps.supervisor.status(),
|
|
72107
|
+
nodeId: deps.nodeId,
|
|
72108
|
+
...downloadProgress !== void 0 ? { downloadProgress } : {},
|
|
72109
|
+
...download !== void 0 ? { download } : {}
|
|
72110
|
+
};
|
|
72111
|
+
}
|
|
72112
|
+
return {
|
|
72113
|
+
complete: async (input) => {
|
|
72114
|
+
const guard = installedGuard(input.runtime);
|
|
72115
|
+
if (!guard.ok) return {
|
|
72116
|
+
ok: false,
|
|
72117
|
+
code: "unavailable",
|
|
72118
|
+
message: guard.message
|
|
72119
|
+
};
|
|
72120
|
+
await ensureStartedInternal(input.runtime);
|
|
72121
|
+
const port = deps.supervisor.port;
|
|
72122
|
+
if (port === void 0) return {
|
|
72123
|
+
ok: false,
|
|
72124
|
+
code: "unavailable",
|
|
72125
|
+
message: "llama-server has no port"
|
|
72126
|
+
};
|
|
72127
|
+
const paths = await resolvePaths(input.runtime);
|
|
72128
|
+
const timeoutMs = input.timeoutMs ?? 12e4;
|
|
72129
|
+
const localProfile = {
|
|
72130
|
+
id: "managed-local",
|
|
72131
|
+
name: "managed-local",
|
|
72132
|
+
kind: "openai-compatible",
|
|
72133
|
+
addonId: "ai",
|
|
72134
|
+
enabled: true,
|
|
72135
|
+
model: paths.modelId,
|
|
72136
|
+
baseUrl: `http://127.0.0.1:${String(port)}/v1`,
|
|
72137
|
+
supportsVision: paths.mmprojPath !== void 0,
|
|
72138
|
+
timeoutMs,
|
|
72139
|
+
connectTimeoutMs: LlmTimeoutDefaults.connectMs,
|
|
72140
|
+
firstTokenTimeoutMs: LlmTimeoutDefaults.firstTokenMs,
|
|
72141
|
+
idleTimeoutMs: LlmTimeoutDefaults.idleMs,
|
|
72142
|
+
retry: {
|
|
72143
|
+
enabled: false,
|
|
72144
|
+
maxAttempts: 1
|
|
72145
|
+
},
|
|
72146
|
+
toolsEnabled: false
|
|
72147
|
+
};
|
|
72148
|
+
const result = await deps.client.generate({
|
|
72149
|
+
profile: localProfile,
|
|
72150
|
+
...input.system !== void 0 ? { system: input.system } : {},
|
|
72151
|
+
prompt: input.prompt,
|
|
72152
|
+
...input.images !== void 0 ? { images: input.images } : {},
|
|
72153
|
+
...input.jsonSchema !== void 0 ? { jsonSchema: input.jsonSchema } : {},
|
|
72154
|
+
...input.maxTokens !== void 0 ? { maxTokens: input.maxTokens } : {},
|
|
72155
|
+
...input.temperature !== void 0 ? { temperature: input.temperature } : {},
|
|
72156
|
+
...input.topP !== void 0 ? { topP: input.topP } : {},
|
|
72157
|
+
...input.topK !== void 0 ? { topK: input.topK } : {},
|
|
72158
|
+
signal: new AbortController().signal
|
|
72159
|
+
}, timeoutMs);
|
|
72160
|
+
deps.supervisor.noteActivity();
|
|
72161
|
+
return result;
|
|
72162
|
+
},
|
|
72163
|
+
ensureStarted: async ({ runtime }) => {
|
|
72164
|
+
const guard = installedGuard(runtime);
|
|
72165
|
+
if (!guard.ok) return {
|
|
72166
|
+
nodeId: deps.nodeId,
|
|
72167
|
+
state: "stopped",
|
|
72168
|
+
lastError: guard.message,
|
|
72169
|
+
crashesInWindow: 0
|
|
72170
|
+
};
|
|
72171
|
+
return ensureStartedInternal(runtime);
|
|
72172
|
+
},
|
|
72173
|
+
stop: async () => {
|
|
72174
|
+
await deps.supervisor.stop();
|
|
72175
|
+
},
|
|
72176
|
+
status: async () => status(),
|
|
72177
|
+
/**
|
|
72178
|
+
* Install a model on THIS node.
|
|
72179
|
+
*
|
|
72180
|
+
* Loud on purpose. This is the longest-running operation the addon has —
|
|
72181
|
+
* tens of minutes for a 23 GB vision model — and until now it emitted not
|
|
72182
|
+
* one log line, so an install that stalled on a gated URL or a full disk
|
|
72183
|
+
* was indistinguishable from one that was simply slow. Every phase
|
|
72184
|
+
* transition is a line, and every line carries the node.
|
|
72185
|
+
*/
|
|
72186
|
+
installModel: async ({ model }) => {
|
|
72187
|
+
const resolution = entryForRef(model);
|
|
72188
|
+
if (resolution === null) throw new Error("unknown model reference");
|
|
72189
|
+
if (resolution.localPathOverride !== void 0) {
|
|
72190
|
+
deps.logger.info("llm model is pre-provisioned; nothing to download", { meta: {
|
|
72191
|
+
nodeId: deps.nodeId,
|
|
72192
|
+
path: resolution.localPathOverride
|
|
72193
|
+
} });
|
|
72194
|
+
return;
|
|
72195
|
+
}
|
|
72196
|
+
const { entry } = resolution;
|
|
72197
|
+
const declaredBytes = model.kind === "url" ? model.sizeBytes : void 0;
|
|
72198
|
+
const startedAt = Date.now();
|
|
72199
|
+
deps.logger.info("llm model install started", { meta: {
|
|
72200
|
+
nodeId: deps.nodeId,
|
|
72201
|
+
modelId: entry.id,
|
|
72202
|
+
url: entry.formats.gguf?.url,
|
|
72203
|
+
extraFiles: (entry.extraFiles ?? []).map((f) => f.filename),
|
|
72204
|
+
...declaredBytes !== void 0 ? {
|
|
72205
|
+
declaredBytes,
|
|
72206
|
+
declaredSize: gb(declaredBytes)
|
|
72207
|
+
} : {}
|
|
72208
|
+
} });
|
|
72209
|
+
downloadProgress = 0;
|
|
72210
|
+
download = {
|
|
72211
|
+
phase: "downloading",
|
|
72212
|
+
file: "",
|
|
72213
|
+
fileIndex: 0,
|
|
72214
|
+
fileCount: 0,
|
|
72215
|
+
downloadedBytes: 0
|
|
72216
|
+
};
|
|
72217
|
+
let lastLoggedDecile = -1;
|
|
72218
|
+
try {
|
|
72219
|
+
await deps.modelOps.ensure(entry, (progress) => {
|
|
72220
|
+
const fraction = progress.totalBytes !== void 0 && progress.totalBytes > 0 ? Math.min(1, progress.downloadedBytes / progress.totalBytes) : void 0;
|
|
72221
|
+
downloadProgress = fraction;
|
|
72222
|
+
download = {
|
|
72223
|
+
phase: "downloading",
|
|
72224
|
+
file: progress.file,
|
|
72225
|
+
fileIndex: progress.fileIndex,
|
|
72226
|
+
fileCount: progress.fileCount,
|
|
72227
|
+
downloadedBytes: progress.downloadedBytes,
|
|
72228
|
+
...progress.totalBytes !== void 0 ? { totalBytes: progress.totalBytes } : {}
|
|
72229
|
+
};
|
|
72230
|
+
const decile = fraction === void 0 ? -1 : Math.floor(fraction * 10);
|
|
72231
|
+
if (decile > lastLoggedDecile) {
|
|
72232
|
+
lastLoggedDecile = decile;
|
|
72233
|
+
deps.logger.info("llm model download progress", { meta: {
|
|
72234
|
+
nodeId: deps.nodeId,
|
|
72235
|
+
modelId: entry.id,
|
|
72236
|
+
file: progress.file,
|
|
72237
|
+
fileIndex: progress.fileIndex,
|
|
72238
|
+
fileCount: progress.fileCount,
|
|
72239
|
+
downloadedBytes: progress.downloadedBytes,
|
|
72240
|
+
downloaded: gb(progress.downloadedBytes),
|
|
72241
|
+
...progress.totalBytes !== void 0 ? { total: gb(progress.totalBytes) } : {}
|
|
72242
|
+
} });
|
|
72243
|
+
}
|
|
72244
|
+
});
|
|
72245
|
+
await verifyDigests(entry, model, startedAt);
|
|
72246
|
+
deps.logger.info("llm model install complete", { meta: {
|
|
72247
|
+
nodeId: deps.nodeId,
|
|
72248
|
+
modelId: entry.id,
|
|
72249
|
+
elapsedMs: Date.now() - startedAt
|
|
72250
|
+
} });
|
|
72251
|
+
} catch (err) {
|
|
72252
|
+
deps.logger.error("llm model install failed", { meta: {
|
|
72253
|
+
nodeId: deps.nodeId,
|
|
72254
|
+
modelId: entry.id,
|
|
72255
|
+
elapsedMs: Date.now() - startedAt,
|
|
72256
|
+
error: err instanceof Error ? err.message : String(err)
|
|
72257
|
+
} });
|
|
72258
|
+
throw err;
|
|
72259
|
+
} finally {
|
|
72260
|
+
downloadProgress = void 0;
|
|
72261
|
+
download = void 0;
|
|
72262
|
+
}
|
|
72263
|
+
},
|
|
72264
|
+
deleteModel: async ({ file }) => {
|
|
72265
|
+
const loaded = deps.supervisor.status().modelPath;
|
|
72266
|
+
if (loaded !== void 0 && basename(loaded) === file) throw new Error(`cannot delete ${file}: loaded by the running runtime`);
|
|
72267
|
+
await fsp.rm(path$1.join(deps.modelsDir, file), { force: true });
|
|
72268
|
+
},
|
|
72269
|
+
listLocalModels: async () => {
|
|
72270
|
+
return (await listGgufFiles(deps.modelsDir)).map((f) => {
|
|
72271
|
+
const catalogId = catalogIdForFile(f.file);
|
|
72272
|
+
return {
|
|
72273
|
+
file: f.file,
|
|
72274
|
+
sizeBytes: f.sizeBytes,
|
|
72275
|
+
path: path$1.join(deps.modelsDir, f.file),
|
|
72276
|
+
...catalogId !== void 0 ? { catalogId } : {}
|
|
72277
|
+
};
|
|
72278
|
+
});
|
|
72279
|
+
},
|
|
72280
|
+
getDiskUsage: async () => {
|
|
72281
|
+
const modelsBytes = (await listGgufFiles(deps.modelsDir)).reduce((sum, f) => sum + f.sizeBytes, 0);
|
|
72282
|
+
return {
|
|
72283
|
+
nodeId: deps.nodeId,
|
|
72284
|
+
modelsBytes
|
|
72285
|
+
};
|
|
72286
|
+
}
|
|
72287
|
+
};
|
|
72288
|
+
}
|
|
72289
|
+
async function listGgufFiles(dir) {
|
|
72290
|
+
let names;
|
|
72291
|
+
try {
|
|
72292
|
+
names = await fsp.readdir(dir);
|
|
72293
|
+
} catch {
|
|
72294
|
+
return [];
|
|
72295
|
+
}
|
|
72296
|
+
const out = [];
|
|
72297
|
+
for (const name of names) {
|
|
72298
|
+
if (!name.endsWith(".gguf")) continue;
|
|
72299
|
+
try {
|
|
72300
|
+
const stat = await fsp.stat(path$1.join(dir, name));
|
|
72301
|
+
out.push({
|
|
72302
|
+
file: name,
|
|
72303
|
+
sizeBytes: stat.size
|
|
72304
|
+
});
|
|
72305
|
+
} catch {}
|
|
72306
|
+
}
|
|
72307
|
+
return out;
|
|
72308
|
+
}
|
|
72309
|
+
//#endregion
|
|
72310
|
+
//#region src/runtime-client.ts
|
|
72311
|
+
/**
|
|
72312
|
+
* `RuntimeClient` over the cap plane — every verb pins the target node with
|
|
72313
|
+
* `nodePin(nodeId)` (transport-level, never a hand-rolled routing field). The
|
|
72314
|
+
* CapRouteResolver classifies the pin into hub-local-uds / remote-moleculer /
|
|
72315
|
+
* agent-child-forward transparently (spec §4.1; the model-studio cross-node
|
|
72316
|
+
* convert precedent). Node enumeration is the `nodes.topology` roster filtered
|
|
72317
|
+
* to nodes advertising the `llm-runtime` cap — never a shadow registry.
|
|
72318
|
+
*/
|
|
72319
|
+
var LLM_RUNTIME_CAP = "llm-runtime";
|
|
72320
|
+
function createRuntimeClient(api) {
|
|
72321
|
+
return {
|
|
72322
|
+
complete: (nodeId, input) => api.llmRuntime.complete.mutate(input, nodePin(nodeId)),
|
|
72323
|
+
ensureStarted: (nodeId, runtime) => api.llmRuntime.ensureStarted.mutate({ runtime }, nodePin(nodeId)),
|
|
72324
|
+
stopRuntime: (nodeId) => api.llmRuntime.stop.mutate({}, nodePin(nodeId)),
|
|
72325
|
+
status: (nodeId) => api.llmRuntime.status.query({}, nodePin(nodeId)),
|
|
72326
|
+
installModel: (nodeId, model) => api.llmRuntime.installModel.mutate({ model }, nodePin(nodeId)),
|
|
72327
|
+
deleteModel: (nodeId, file) => api.llmRuntime.deleteModel.mutate({ file }, nodePin(nodeId)),
|
|
72328
|
+
listLocalModels: (nodeId) => api.llmRuntime.listLocalModels.query({}, nodePin(nodeId)),
|
|
72329
|
+
getDiskUsage: (nodeId) => api.llmRuntime.getDiskUsage.query({}, nodePin(nodeId)),
|
|
72330
|
+
listRuntimeNodeIds: async () => {
|
|
72331
|
+
const topology = await api.nodes.topology.query();
|
|
72332
|
+
const ids = /* @__PURE__ */ new Set();
|
|
72333
|
+
for (const node of topology) if (node.addons.some((a) => a.capabilities.includes(LLM_RUNTIME_CAP))) ids.add(node.id);
|
|
72334
|
+
return [...ids];
|
|
72335
|
+
}
|
|
72336
|
+
};
|
|
72337
|
+
}
|
|
72338
|
+
//#endregion
|
|
72339
|
+
//#region src/assembly.ts
|
|
72340
|
+
/**
|
|
72341
|
+
* Registration assembly (the hub/agent split, spec §1). Every node running
|
|
72342
|
+
* addon-ai registers `llm-runtime`; ONLY the hub also registers the consumer
|
|
72343
|
+
* `llm` surface (profiles/usage need outbound internet + API keys).
|
|
72344
|
+
* Extracted from the addon class so the split + seeding is unit-testable
|
|
72345
|
+
* without a full AddonContext.
|
|
72346
|
+
*/
|
|
72347
|
+
async function assembleAi(deps) {
|
|
72348
|
+
const client = deps.client ?? createLlmClient();
|
|
72349
|
+
const runtimeProvider = createLlmRuntimeProvider({
|
|
72350
|
+
nodeId: deps.nodeId,
|
|
72351
|
+
modelsDir: deps.modelsDir,
|
|
72352
|
+
ensureBinary: deps.ensureBinary,
|
|
72353
|
+
supervisor: deps.supervisor,
|
|
72354
|
+
modelOps: createDefaultModelOps(deps.modelsDir),
|
|
72355
|
+
client,
|
|
72356
|
+
logger: deps.logger.child("llm-runtime")
|
|
72357
|
+
});
|
|
72358
|
+
const registrations = [{
|
|
72359
|
+
capability: llmRuntimeCapability,
|
|
72360
|
+
provider: runtimeProvider
|
|
72361
|
+
}];
|
|
72362
|
+
if (!deps.isHub) return {
|
|
72363
|
+
registrations,
|
|
72364
|
+
runtimeProvider
|
|
72365
|
+
};
|
|
72366
|
+
const { UsageStore } = await import("./usage-store-RiVXP_ma.mjs").then((n) => n.i);
|
|
72367
|
+
const store = new ProfileStore(deps.settingsPort);
|
|
72368
|
+
const defaults = new DefaultsStore(deps.settingsPort);
|
|
72369
|
+
const usage = new UsageStore(deps.settingsPort, Date.now, deps.logger.child("llm-usage"));
|
|
72370
|
+
await store.init();
|
|
72371
|
+
await defaults.init();
|
|
72372
|
+
await usage.init();
|
|
72373
|
+
await store.ensureSeeded();
|
|
72374
|
+
const llmProvider = createLlmProvider({
|
|
72375
|
+
store,
|
|
72376
|
+
defaults,
|
|
72377
|
+
usage,
|
|
72378
|
+
client,
|
|
72379
|
+
...deps.runtimeApi !== void 0 ? { runtime: createRuntimeClient(deps.runtimeApi) } : {},
|
|
72380
|
+
...deps.distributeModel !== void 0 ? { distributeModel: deps.distributeModel } : {},
|
|
72381
|
+
catalog: LLM_MODEL_CATALOG.map((m) => m.meta),
|
|
72382
|
+
hfToken: () => process.env["HF_TOKEN"] ?? process.env["HUGGING_FACE_HUB_TOKEN"],
|
|
72383
|
+
logger: deps.logger.child("llm")
|
|
72384
|
+
});
|
|
72385
|
+
registrations.push({
|
|
72386
|
+
capability: llmCapability,
|
|
72387
|
+
provider: llmProvider
|
|
72388
|
+
});
|
|
72389
|
+
return {
|
|
72390
|
+
registrations,
|
|
72391
|
+
runtimeProvider,
|
|
72392
|
+
llmProvider,
|
|
72393
|
+
store,
|
|
72394
|
+
usage,
|
|
72395
|
+
prune: (retentionDays) => usage.prune(retentionDays)
|
|
72396
|
+
};
|
|
72397
|
+
}
|
|
72398
|
+
//#endregion
|
|
71361
72399
|
//#region src/settings-store-port.ts
|
|
71362
72400
|
function createApiSettingsStorePort(api) {
|
|
71363
72401
|
return {
|
|
@@ -71462,7 +72500,9 @@ var TestChatImageRefSchema = discriminatedUnion("kind", [object({
|
|
|
71462
72500
|
deviceId: number$1().int().positive()
|
|
71463
72501
|
}), object({
|
|
71464
72502
|
kind: literal("track"),
|
|
71465
|
-
trackId: string().min(1)
|
|
72503
|
+
trackId: string().min(1),
|
|
72504
|
+
/** Owning device — `getTrackMedia` requires it as the read's scope. */
|
|
72505
|
+
deviceId: number$1().int().positive()
|
|
71466
72506
|
})]);
|
|
71467
72507
|
/**
|
|
71468
72508
|
* `.strict()`, and it is load-bearing.
|
|
@@ -71688,7 +72728,7 @@ async function resolveImage(deps, ref) {
|
|
|
71688
72728
|
}
|
|
71689
72729
|
let rows;
|
|
71690
72730
|
try {
|
|
71691
|
-
rows = await deps.getTrackMedia(ref.trackId, TRACK_MEDIA_PREFERENCE);
|
|
72731
|
+
rows = await deps.getTrackMedia(ref.trackId, TRACK_MEDIA_PREFERENCE, ref.deviceId);
|
|
71692
72732
|
} catch (cause) {
|
|
71693
72733
|
const message = cause instanceof Error ? cause.message : String(cause);
|
|
71694
72734
|
deps.logger.warn("ai test chat: track media read failed — turn dropped", { meta: {
|
|
@@ -72175,7 +73215,7 @@ var AiAddon = class extends BaseAddon {
|
|
|
72175
73215
|
const settingsPort = api !== void 0 ? createApiSettingsStorePort(api) : createMemorySettingsStorePort();
|
|
72176
73216
|
if (api === void 0) logger.warn("addon-ai: no ctx.api — profiles are in-memory only");
|
|
72177
73217
|
const binDir = path$1.join(ctx.nodeDataDir, "bin");
|
|
72178
|
-
const { ensureLlamaServer } = await import("./ensure-llama-server-
|
|
73218
|
+
const { ensureLlamaServer } = await import("./ensure-llama-server-COC6iveo.mjs").then((n) => n.r);
|
|
72179
73219
|
const assembly = await assembleAi({
|
|
72180
73220
|
nodeId: ownNodeId,
|
|
72181
73221
|
isHub,
|
|
@@ -72283,9 +73323,10 @@ var AiAddon = class extends BaseAddon {
|
|
|
72283
73323
|
});
|
|
72284
73324
|
},
|
|
72285
73325
|
getSnapshot: (deviceId) => api.snapshot.getSnapshot.query({ deviceId }),
|
|
72286
|
-
getTrackMedia: (trackId, kinds) => api.pipelineAnalytics.getTrackMedia.query({
|
|
73326
|
+
getTrackMedia: (trackId, kinds, deviceId) => api.pipelineAnalytics.getTrackMedia.query({
|
|
72287
73327
|
trackId,
|
|
72288
|
-
kinds: [...kinds]
|
|
73328
|
+
kinds: [...kinds],
|
|
73329
|
+
deviceId
|
|
72289
73330
|
}),
|
|
72290
73331
|
recordUsage: (row) => usage.record(row),
|
|
72291
73332
|
logger,
|
|
@@ -72336,4 +73377,4 @@ var AiAddon = class extends BaseAddon {
|
|
|
72336
73377
|
}
|
|
72337
73378
|
};
|
|
72338
73379
|
//#endregion
|
|
72339
|
-
export { resolveRetryPolicy as A, AiAddon, AiAddon as default, __commonJSMin as B, createDefaultModelOps as C, AI_ADDON_ID as D, entryForRef as E, LlmProfileKindSchema as F, boolean as I, number$1 as L, require_token_util as M, require_token_error as N, createLlmProvider as O, LlmErrorCodeSchema as P, object as R,
|
|
73380
|
+
export { resolveRetryPolicy as A, AiAddon, AiAddon as default, __commonJSMin as B, createDefaultModelOps as C, AI_ADDON_ID as D, entryForRef as E, LlmProfileKindSchema as F, boolean as I, number$1 as L, require_token_util as M, require_token_error as N, createLlmProvider as O, LlmErrorCodeSchema as P, object as R, LlamaSupervisor as S, catalogById as T, __exportAll as V, createApiSettingsStorePort as _, renderTranscript as a, createLlmRuntimeProvider as b, TEST_CHAT_CONSUMER as c, TEST_CHAT_MAX_FIRST_TOKEN_TIMEOUT_MS as d, TEST_CHAT_MIN_VISION_INPUT_TOKENS as f, encodeEvent as g, TestChatRequestSchema as h, pickTrackMedia as i, createLlmClient as j, CONSUMER_RETRY_POLICY as k, TEST_CHAT_DEFAULT_FIRST_TOKEN_TIMEOUT_MS as l, TestChatEventSchema as m, runTestChatStream as n, resolveImage as o, TEST_CHAT_PREFIX as p, TRACK_MEDIA_PREFERENCE as r, TEST_CHAT_CONNECT_TIMEOUT_MS as s, createTestChatPlaneHandler as t, TEST_CHAT_IDLE_TIMEOUT_MS as u, createMemorySettingsStorePort as v, LLM_MODEL_CATALOG as w, fileSha256 as x, createRuntimeClient as y, string as z };
|