@camstack/addon-ai 0.4.2 → 0.4.4
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 +1759 -477
- package/dist/addon.mjs +1762 -480
- 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
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
import * as path$1 from "node:path";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
|
-
import {
|
|
4
|
+
import { connect, createServer } from "node:net";
|
|
5
5
|
import * as fs from "node:fs";
|
|
6
|
-
import { createReadStream } from "node:fs";
|
|
6
|
+
import { createReadStream, existsSync, statSync } from "node:fs";
|
|
7
|
+
import { promisify } from "node:util";
|
|
7
8
|
import { brotliCompress, gzip } from "node:zlib";
|
|
8
9
|
import * as fsp from "node:fs/promises";
|
|
9
10
|
import { spawn } from "node:child_process";
|
|
10
|
-
import { createServer } from "node:net";
|
|
11
11
|
//#region \0rolldown/runtime.js
|
|
12
12
|
var __create = Object.create;
|
|
13
13
|
var __defProp$1 = Object.defineProperty;
|
|
@@ -12500,6 +12500,18 @@ var LlmGenerateBaseInputSchema = object({
|
|
|
12500
12500
|
* Resource ceiling = llama-server flags + idleStopMinutes ONLY (no RSS
|
|
12501
12501
|
* watchdog — operator decision #3).
|
|
12502
12502
|
*/
|
|
12503
|
+
/**
|
|
12504
|
+
* A companion artifact that MUST land beside the main GGUF: the `mmproj`
|
|
12505
|
+
* projector of a vision model, or shards 2..N of a split GGUF. Carried on the
|
|
12506
|
+
* REF rather than looked up at install time, so what the operator approved in
|
|
12507
|
+
* the preview is exactly what the node downloads.
|
|
12508
|
+
*/
|
|
12509
|
+
var ManagedModelExtraFileSchema = object({
|
|
12510
|
+
url: string(),
|
|
12511
|
+
filename: string(),
|
|
12512
|
+
sizeBytes: number$1(),
|
|
12513
|
+
sha256: string().optional()
|
|
12514
|
+
});
|
|
12503
12515
|
var ManagedModelRefSchema = discriminatedUnion("kind", [
|
|
12504
12516
|
object({
|
|
12505
12517
|
kind: literal("catalog"),
|
|
@@ -12508,7 +12520,11 @@ var ManagedModelRefSchema = discriminatedUnion("kind", [
|
|
|
12508
12520
|
object({
|
|
12509
12521
|
kind: literal("url"),
|
|
12510
12522
|
url: string(),
|
|
12511
|
-
sha256: string().optional()
|
|
12523
|
+
sha256: string().optional(),
|
|
12524
|
+
/** Picker/status label; the file basename when absent. */
|
|
12525
|
+
label: string().optional(),
|
|
12526
|
+
sizeBytes: number$1().optional(),
|
|
12527
|
+
extraFiles: array(ManagedModelExtraFileSchema).optional()
|
|
12512
12528
|
}),
|
|
12513
12529
|
object({
|
|
12514
12530
|
kind: literal("path"),
|
|
@@ -12569,11 +12585,39 @@ var ManagedRuntimeConfigSchema = object({
|
|
|
12569
12585
|
"q4_1",
|
|
12570
12586
|
"q4_0"
|
|
12571
12587
|
]).optional(),
|
|
12588
|
+
/**
|
|
12589
|
+
* Escape hatch for llama-server flags this schema does NOT model — `--jinja`
|
|
12590
|
+
* (which most vision chat templates need and some language-only models
|
|
12591
|
+
* dislike), `--cont-batching`, `--rope-scaling`, …
|
|
12592
|
+
*
|
|
12593
|
+
* It is NOT a second place to set the flags above. A token that collides
|
|
12594
|
+
* with a typed field is REJECTED at start, naming the field that owns it
|
|
12595
|
+
* (`assertNoOwnedFlags`), because two knobs writing the same argv is exactly
|
|
12596
|
+
* the "two switches that disagree" failure this repo has already shipped
|
|
12597
|
+
* twice (D62).
|
|
12598
|
+
*/
|
|
12599
|
+
extraArgs: array(string()).default([]),
|
|
12572
12600
|
/** Else lazy: first generate boots it. */
|
|
12573
12601
|
autoStart: boolean().default(false),
|
|
12574
12602
|
/** 0 = never; frees RAM after quiet periods. */
|
|
12575
12603
|
idleStopMinutes: number$1().int().default(30)
|
|
12576
12604
|
});
|
|
12605
|
+
/**
|
|
12606
|
+
* Where a multi-GB install currently is. A single 0..1 fraction cannot answer
|
|
12607
|
+
* "is it stuck?" for an install that is three files (shards + mmproj) followed
|
|
12608
|
+
* by a sha256 pass over 22 GB — during which the fraction sat at 1.0 and the
|
|
12609
|
+
* node looked hung. Phase + file + bytes is the smallest shape that does.
|
|
12610
|
+
*/
|
|
12611
|
+
var LlmDownloadProgressSchema = object({
|
|
12612
|
+
phase: _enum(["downloading", "verifying"]),
|
|
12613
|
+
/** The artifact currently moving, e.g. `mmproj-F16.gguf`. */
|
|
12614
|
+
file: string(),
|
|
12615
|
+
fileIndex: number$1().int(),
|
|
12616
|
+
fileCount: number$1().int(),
|
|
12617
|
+
/** Across the WHOLE install, not the current file. */
|
|
12618
|
+
downloadedBytes: number$1(),
|
|
12619
|
+
totalBytes: number$1().optional()
|
|
12620
|
+
});
|
|
12577
12621
|
var LlmRuntimeStatusSchema = object({
|
|
12578
12622
|
/** Status is ALWAYS node-qualified. */
|
|
12579
12623
|
nodeId: string(),
|
|
@@ -12590,6 +12634,8 @@ var LlmRuntimeStatusSchema = object({
|
|
|
12590
12634
|
modelPath: string().optional(),
|
|
12591
12635
|
modelId: string().optional(),
|
|
12592
12636
|
downloadProgress: number$1().min(0).max(1).optional(),
|
|
12637
|
+
/** Detail behind `downloadProgress`; present for the same lifetime. */
|
|
12638
|
+
download: LlmDownloadProgressSchema.optional(),
|
|
12593
12639
|
lastError: string().optional(),
|
|
12594
12640
|
crashesInWindow: number$1(),
|
|
12595
12641
|
/** Child RSS (sampled best-effort). */
|
|
@@ -12600,7 +12646,14 @@ var LlmNodeModelSchema = object({
|
|
|
12600
12646
|
file: string(),
|
|
12601
12647
|
sizeBytes: number$1(),
|
|
12602
12648
|
catalogId: string().optional(),
|
|
12603
|
-
installedAt: number$1().optional()
|
|
12649
|
+
installedAt: number$1().optional(),
|
|
12650
|
+
/**
|
|
12651
|
+
* Absolute path on the node. Present so a file that is on disk but matches
|
|
12652
|
+
* no catalog entry — a custom Hugging Face install, or a GGUF the operator
|
|
12653
|
+
* copied in by hand — is still SELECTABLE, as a `{kind:'path'}` ref. Without
|
|
12654
|
+
* it the picker could list such a file and do nothing with it.
|
|
12655
|
+
*/
|
|
12656
|
+
path: string().optional()
|
|
12604
12657
|
});
|
|
12605
12658
|
var LlmRuntimeDiskUsageSchema = object({
|
|
12606
12659
|
nodeId: string(),
|
|
@@ -12693,9 +12746,12 @@ var LlmProfileSchema = object({
|
|
|
12693
12746
|
systemPrompt: string().optional(),
|
|
12694
12747
|
/** Total generation bound — the only one a unary call has. */
|
|
12695
12748
|
timeoutMs: number$1().int().positive().default(6e4),
|
|
12696
|
-
/**
|
|
12749
|
+
/** The TCP handshake only — "is the port even open". NOT the wait for
|
|
12750
|
+
* response headers: on the LM Studio / llama-server wire those are written
|
|
12751
|
+
* once the model has finished loading, so they belong to the bound below. */
|
|
12697
12752
|
connectTimeoutMs: number$1().int().positive().default(1e4),
|
|
12698
|
-
/** Accepted, but no output yet —
|
|
12753
|
+
/** Accepted, but no output yet — response headers included, because a cold
|
|
12754
|
+
* GPU load is exactly what happens before them. */
|
|
12699
12755
|
firstTokenTimeoutMs: number$1().int().positive().default(12e4),
|
|
12700
12756
|
/** Output started then stopped. */
|
|
12701
12757
|
idleTimeoutMs: number$1().int().positive().default(6e4),
|
|
@@ -12758,6 +12814,36 @@ var ManagedModelCatalogEntrySchema = object({
|
|
|
12758
12814
|
/** Vision models: companion projector file. */
|
|
12759
12815
|
mmprojUrl: string().optional()
|
|
12760
12816
|
});
|
|
12817
|
+
/**
|
|
12818
|
+
* The outcome of turning one operator-typed Hugging Face reference into a
|
|
12819
|
+
* download plan. A RESULT, never a throw: "this repo has 24 quantizations and
|
|
12820
|
+
* I will not pick for you" is a normal answer the UI has to render, not an
|
|
12821
|
+
* exception.
|
|
12822
|
+
*
|
|
12823
|
+
* `candidates` is the whole reason the refusal is usable — every string in it
|
|
12824
|
+
* is a tag that resolves when pasted back as `<org>/<repo>:<TAG>`.
|
|
12825
|
+
*/
|
|
12826
|
+
var HfModelResolutionSchema = discriminatedUnion("ok", [object({
|
|
12827
|
+
ok: literal(true),
|
|
12828
|
+
/** Ready to hand to `installModel` unchanged. */
|
|
12829
|
+
model: ManagedModelRefSchema,
|
|
12830
|
+
label: string(),
|
|
12831
|
+
repo: string(),
|
|
12832
|
+
quantization: string(),
|
|
12833
|
+
purpose: _enum(["text", "vision"]),
|
|
12834
|
+
totalBytes: number$1(),
|
|
12835
|
+
/** mmproj + shards, for the preview: an operator approving 23 GB should
|
|
12836
|
+
* see that 0.9 GB of it is a projector they did not name. */
|
|
12837
|
+
extraFilenames: array(string())
|
|
12838
|
+
}), object({
|
|
12839
|
+
ok: literal(false),
|
|
12840
|
+
code: string(),
|
|
12841
|
+
message: string(),
|
|
12842
|
+
candidates: array(string()).optional(),
|
|
12843
|
+
/** Set when the refusal was only the ceiling: re-calling with
|
|
12844
|
+
* `maxBytes: requiredBytes` is the operator's explicit override. */
|
|
12845
|
+
requiredBytes: number$1().optional()
|
|
12846
|
+
})]);
|
|
12761
12847
|
var LlmRuntimeNodeSchema = object({
|
|
12762
12848
|
nodeId: string(),
|
|
12763
12849
|
reachable: boolean(),
|
|
@@ -12825,6 +12911,25 @@ var llmCapability = {
|
|
|
12825
12911
|
listModelCatalog: method(object({}), array(ManagedModelCatalogEntrySchema)),
|
|
12826
12912
|
listRuntimeNodes: method(object({}), array(LlmRuntimeNodeSchema)),
|
|
12827
12913
|
listNodeModels: method(object({ nodeId: string() }), array(LlmNodeModelSchema)),
|
|
12914
|
+
/**
|
|
12915
|
+
* One typed Hugging Face reference → a pinned, verified `ManagedModelRef`.
|
|
12916
|
+
*
|
|
12917
|
+
* Runs on the HUB, not on the target node: resolution needs egress to
|
|
12918
|
+
* huggingface.co, and an agent that cannot reach it still installs fine
|
|
12919
|
+
* through the model-distributor relay. Nothing is downloaded here — this is
|
|
12920
|
+
* a tree read plus a HEAD, so the operator sees the size, the quantization
|
|
12921
|
+
* and the mmproj BEFORE approving a multi-GB pull.
|
|
12922
|
+
*/
|
|
12923
|
+
resolveModelRef: method(object({
|
|
12924
|
+
/** `https://huggingface.co/<org>/<repo>/resolve/main/<f>.gguf`,
|
|
12925
|
+
* `<org>/<repo>/<f>.gguf`, `<org>/<repo>` or `<org>/<repo>:<QUANT>`. */
|
|
12926
|
+
ref: string(),
|
|
12927
|
+
/** Explicit ceiling override, in bytes. Absent = the built-in ceiling. */
|
|
12928
|
+
maxBytes: number$1().positive().optional()
|
|
12929
|
+
}), HfModelResolutionSchema, {
|
|
12930
|
+
kind: "mutation",
|
|
12931
|
+
auth: "admin"
|
|
12932
|
+
}),
|
|
12828
12933
|
installModel: method(object({
|
|
12829
12934
|
nodeId: string(),
|
|
12830
12935
|
model: ManagedModelRefSchema
|
|
@@ -14728,13 +14833,81 @@ var NcRuleActionsSchema = object({
|
|
|
14728
14833
|
*/
|
|
14729
14834
|
buttons: array(NcRuleNotificationButtonSchema).max(8).optional()
|
|
14730
14835
|
});
|
|
14836
|
+
/**
|
|
14837
|
+
* "This rule applies only while `deviceId` is in one of `states`."
|
|
14838
|
+
*
|
|
14839
|
+
* The states are the DEVICE's own vocabulary — `AlarmState` for a panel,
|
|
14840
|
+
* `on`/`off` for a switch — not a normalised set, because normalising would
|
|
14841
|
+
* make the condition lie about devices whose states have no equivalent.
|
|
14842
|
+
*
|
|
14843
|
+
* An unreadable state does NOT match: see the engine's fail-closed gate. A
|
|
14844
|
+
* condition that fired on "I could not read it" would be worse than no gate.
|
|
14845
|
+
*/
|
|
14846
|
+
var NcDeviceStateConditionSchema = object({
|
|
14847
|
+
deviceId: number$1().int(),
|
|
14848
|
+
/** Any of these matches. */
|
|
14849
|
+
states: array(string().min(1)).min(1)
|
|
14850
|
+
});
|
|
14851
|
+
/**
|
|
14852
|
+
* "This rule applies only while scene `sceneId` is `matched` / `diverged`."
|
|
14853
|
+
*
|
|
14854
|
+
* A GATE, not a trigger. `occupancy` and `audio` each DISCRIMINATE their rule —
|
|
14855
|
+
* carrying one makes the rule fire on that subject and nothing else. Scene is
|
|
14856
|
+
* the other shape entirely, the `deviceState` shape: it narrows a rule that
|
|
14857
|
+
* already has a trigger ("tell me about a person at the front door, but only
|
|
14858
|
+
* while the bin is still out"). That is why it composes with every delivery
|
|
14859
|
+
* instead of owning one, and why no new `NcDelivery` member and no new subject
|
|
14860
|
+
* kind exist for it — see D159.
|
|
14861
|
+
*
|
|
14862
|
+
* ── Identity ───────────────────────────────────────────────────────────────
|
|
14863
|
+
* `sceneId` is `SceneMonitor.id`, a `randomUUID()` minted by `createScene` —
|
|
14864
|
+
* globally unique, so it needs no device to disambiguate it. `deviceId` is
|
|
14865
|
+
* carried as a HINT for the editor and for the log line, never as part of the
|
|
14866
|
+
* lookup key: a rule whose hint drifted must still gate correctly.
|
|
14867
|
+
*
|
|
14868
|
+
* ── Which boolean ──────────────────────────────────────────────────────────
|
|
14869
|
+
* `latched` ABSENT means "whatever the scene itself says" — `SceneMonitor.emit`
|
|
14870
|
+
* already declares which boolean drives notification rules, and a second knob
|
|
14871
|
+
* that could disagree with it is exactly the D62 failure. Set it only to
|
|
14872
|
+
* override one rule against the scene's own default.
|
|
14873
|
+
*
|
|
14874
|
+
* - LIVE reading (`emit`/`latched` resolve to live): passes iff
|
|
14875
|
+
* `verdict === requiredState`. `unknown` — no reference for this light, view
|
|
14876
|
+
* shifted, no snapshot — passes NEITHER. A scene that cannot judge is not
|
|
14877
|
+
* evidence, in either direction.
|
|
14878
|
+
* - LATCHED reading: passes iff `latched === (requiredState === 'diverged')`.
|
|
14879
|
+
* The latch is a durable fact about the past ("it has diverged since I armed
|
|
14880
|
+
* it"), so a camera that has gone dark does not clear it — that is the whole
|
|
14881
|
+
* reason the operator asked for a latch.
|
|
14882
|
+
*
|
|
14883
|
+
* The gate reads an in-memory mirror (`NcSceneStateCache`) refreshed OFF the
|
|
14884
|
+
* event path, never the cap: D49. A mirror that has never loaded, or a scene it
|
|
14885
|
+
* does not carry, reads absent and the rule does NOT fire — fail closed, and
|
|
14886
|
+
* said out loud in the log rather than dropped in silence.
|
|
14887
|
+
*/
|
|
14888
|
+
var NcSceneConditionSchema = object({
|
|
14889
|
+
/** `SceneMonitor.id` — the uuid the cap mints. The whole lookup key. */
|
|
14890
|
+
sceneId: string().min(1),
|
|
14891
|
+
/** The camera the scene lives on. A hint for the editor and the log line. */
|
|
14892
|
+
deviceId: number$1().int().optional(),
|
|
14893
|
+
/** The state the scene must be in for the rule to fire. */
|
|
14894
|
+
requiredState: _enum(["matched", "diverged"]),
|
|
14895
|
+
/**
|
|
14896
|
+
* Read the LATCH (`true`) or the LIVE verdict (`false`). Absent = follow the
|
|
14897
|
+
* scene's own `emit` field, which is the only place that decision belongs.
|
|
14898
|
+
*/
|
|
14899
|
+
latched: boolean().optional()
|
|
14900
|
+
});
|
|
14731
14901
|
var NcConditionsSchema = object({
|
|
14732
14902
|
/** Gate on ANOTHER device's current state (the alarm armed, a switch on). */
|
|
14733
|
-
deviceState:
|
|
14734
|
-
|
|
14735
|
-
|
|
14736
|
-
|
|
14737
|
-
|
|
14903
|
+
deviceState: NcDeviceStateConditionSchema.optional(),
|
|
14904
|
+
/**
|
|
14905
|
+
* Gate on a SCENE's state — "only while the bin is still out". Composes with
|
|
14906
|
+
* every trigger (detection, occupancy, audio, sensor, package, track-end);
|
|
14907
|
+
* unlike `occupancy`/`audio` it discriminates nothing. See
|
|
14908
|
+
* {@link NcSceneCondition} and D159.
|
|
14909
|
+
*/
|
|
14910
|
+
scene: NcSceneConditionSchema.optional(),
|
|
14738
14911
|
/** Device scope — absent = all devices. */
|
|
14739
14912
|
devices: array(number$1()).optional(),
|
|
14740
14913
|
/** Detector class names (any overlap with the record's class set). */
|
|
@@ -15372,6 +15545,7 @@ var NcConditionDescriptorSchema = object({
|
|
|
15372
15545
|
"occupancy",
|
|
15373
15546
|
"audio",
|
|
15374
15547
|
"deviceState",
|
|
15548
|
+
"scene",
|
|
15375
15549
|
"systemEvent"
|
|
15376
15550
|
]),
|
|
15377
15551
|
operator: _enum([
|
|
@@ -17895,6 +18069,17 @@ var maxSessionHoldMsField = {
|
|
|
17895
18069
|
default: 12e4,
|
|
17896
18070
|
step: 5e3
|
|
17897
18071
|
};
|
|
18072
|
+
/**
|
|
18073
|
+
* Quiet period that closes an `audioMode: 'on-motion'` audio window. Floor of
|
|
18074
|
+
* 5s so a rearm can never degenerate into per-event stream churn; default 90s
|
|
18075
|
+
* comfortably outlives the gap between two PIR wakes on a battery camera.
|
|
18076
|
+
*/
|
|
18077
|
+
var audioMotionWindowMsField = {
|
|
18078
|
+
min: 5e3,
|
|
18079
|
+
max: 6e5,
|
|
18080
|
+
default: 9e4,
|
|
18081
|
+
step: 5e3
|
|
18082
|
+
};
|
|
17898
18083
|
var motionFpsField = {
|
|
17899
18084
|
min: 1,
|
|
17900
18085
|
max: 30,
|
|
@@ -18071,6 +18256,27 @@ var RunnerCameraConfigSchema = object({
|
|
|
18071
18256
|
* resolved `CameraDetectionConfig`.
|
|
18072
18257
|
*/
|
|
18073
18258
|
maxSessionHoldMs: number$1().min(maxSessionHoldMsField.min).max(maxSessionHoldMsField.max).optional(),
|
|
18259
|
+
/**
|
|
18260
|
+
* Orchestrator-side quiet period (ms) that closes an `audioMode:
|
|
18261
|
+
* 'on-motion'` audio window, measured from the LAST motion event.
|
|
18262
|
+
*
|
|
18263
|
+
* This exists because the falling edge cannot be relied on. Camera-native
|
|
18264
|
+
* providers emit motion as a RISING EDGE ONLY (Reolink's Baichuan push and
|
|
18265
|
+
* its email-push SMTP path both emit `detected: true` and never the
|
|
18266
|
+
* counterpart); only the frame-diff analyzer emits falls. So on an
|
|
18267
|
+
* onboard-only camera a window that closed only on `detected: false` never
|
|
18268
|
+
* closed at all, and `on-motion` silently behaved as `always-on` — on a
|
|
18269
|
+
* battery camera, the one failure mode the mode exists to prevent.
|
|
18270
|
+
*
|
|
18271
|
+
* Every motion event rearms this timer WITHOUT restarting the stream, so a
|
|
18272
|
+
* burst of re-fires costs nothing. A falling edge, when one does arrive,
|
|
18273
|
+
* still closes earlier via `motionCooldownMs` — whichever comes first wins.
|
|
18274
|
+
*
|
|
18275
|
+
* Not consumed by the runner: carried here so it shares the per-camera
|
|
18276
|
+
* device-settings surface with `motionCooldownMs`, exactly like
|
|
18277
|
+
* `maxSessionHoldMs`.
|
|
18278
|
+
*/
|
|
18279
|
+
audioMotionWindowMs: number$1().min(audioMotionWindowMsField.min).max(audioMotionWindowMsField.max).optional(),
|
|
18074
18280
|
motionFps: number$1().min(motionFpsField.min).max(motionFpsField.max).default(motionFpsField.default),
|
|
18075
18281
|
detectionFps: number$1().min(detectionFpsField.min).max(detectionFpsField.max).default(detectionFpsField.default),
|
|
18076
18282
|
motionStreamId: string(),
|
|
@@ -18166,7 +18372,7 @@ var RunnerCameraConfigSchema = object({
|
|
|
18166
18372
|
*/
|
|
18167
18373
|
inferenceDevices: array(RunnerInferenceDeviceSchema).readonly().optional()
|
|
18168
18374
|
});
|
|
18169
|
-
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;
|
|
18375
|
+
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;
|
|
18170
18376
|
/**
|
|
18171
18377
|
* Runtime load summary returned by `getLocalLoad`. Used by the orchestrator's
|
|
18172
18378
|
* load-balancing levels (L2 capacity-based, L3 hardware-aware) to decide
|
|
@@ -24572,6 +24778,33 @@ method(object({
|
|
|
24572
24778
|
* as `unknown`, never guessed. A day reference scored against an IR frame
|
|
24573
24779
|
* collapses the cosine and would latch a false alarm every single night. */
|
|
24574
24780
|
var SceneConditionSchema = string();
|
|
24781
|
+
/**
|
|
24782
|
+
* What a scene does when the CURRENT light has no reference of its own.
|
|
24783
|
+
*
|
|
24784
|
+
* The lighting variants are not equally likely to exist. Almost every operator
|
|
24785
|
+
* captures daylight and then never stands outside at 22:00 to capture IR, and a
|
|
24786
|
+
* scene that is only ever going to be asked about a daytime question ("is the
|
|
24787
|
+
* bin still on the kerb at 08:00") does not need a night reference at all. The
|
|
24788
|
+
* night half must therefore be OPTIONAL, and optional means the scene keeps
|
|
24789
|
+
* working without it rather than degrading into a permanent complaint.
|
|
24790
|
+
*
|
|
24791
|
+
* - `skip` (default) — the check in that light is not made. Not a verdict, not
|
|
24792
|
+
* an alarm, not even an `unknown`: the live state simply stays whatever the
|
|
24793
|
+
* last covered light left it at, the latch is untouched, and the hysteresis
|
|
24794
|
+
* run is neither spent nor cleared. The scene resumes by itself at first
|
|
24795
|
+
* light. This is the only behaviour under which "I never captured IR" is a
|
|
24796
|
+
* configuration choice instead of a nightly fault.
|
|
24797
|
+
* - `judge-anyway` — score against the OTHER conditions' references. Available
|
|
24798
|
+
* for cameras whose IR frame is close enough to daylight (a floodlit
|
|
24799
|
+
* driveway, an always-white-light doorbell), and wrong for everything else:
|
|
24800
|
+
* cross-condition cosines are not comparable, so a day reference against a
|
|
24801
|
+
* true IR frame collapses and the scene reports a theft at 21:40.
|
|
24802
|
+
*
|
|
24803
|
+
* Never applies when the scene has NO comparable reference at all — that is
|
|
24804
|
+
* "not armed yet", it is reported as `no-reference-for-condition`, and silence
|
|
24805
|
+
* there would hide a scene the operator never finished setting up.
|
|
24806
|
+
*/
|
|
24807
|
+
var SceneUncoveredPolicySchema = _enum(["skip", "judge-anyway"]);
|
|
24575
24808
|
/** `matched` = the baseline is what we see; `diverged` = it demonstrably is not;
|
|
24576
24809
|
* `unknown` = we cannot judge (no reference for this condition, encoder model
|
|
24577
24810
|
* changed, view shifted, no snapshot). `unknown` is a real value, not a null,
|
|
@@ -24627,6 +24860,9 @@ var SceneCheckSchema = discriminatedUnion("mode", [object({
|
|
|
24627
24860
|
hysteresisCount: number$1().int().positive()
|
|
24628
24861
|
})]);
|
|
24629
24862
|
var SCENE_DEFAULT_ANCHOR_THRESHOLD = .85;
|
|
24863
|
+
/** Night is OPTIONAL. A scene with only a daylight reference sits the IR hours
|
|
24864
|
+
* out in silence rather than reporting a fault every night. */
|
|
24865
|
+
var SCENE_DEFAULT_UNCOVERED_POLICY = "skip";
|
|
24630
24866
|
/**
|
|
24631
24867
|
* Vision-model adjudication of a candidate flip. Field names deliberately
|
|
24632
24868
|
* mirror `NcConfirmSchema` so an operator meets one vocabulary, not two.
|
|
@@ -24693,6 +24929,21 @@ var SceneMonitorSchema = object({
|
|
|
24693
24929
|
* automation can react to the bin coming back without the operator's own
|
|
24694
24930
|
* alarm silently clearing itself. */
|
|
24695
24931
|
autoRestore: boolean().default(false),
|
|
24932
|
+
/** What to do when the current light has no reference of its own. See
|
|
24933
|
+
* {@link SceneUncoveredPolicySchema} — the default makes night OPTIONAL. */
|
|
24934
|
+
onUncoveredCondition: SceneUncoveredPolicySchema.default(SCENE_DEFAULT_UNCOVERED_POLICY),
|
|
24935
|
+
/**
|
|
24936
|
+
* The light whose checks are currently being SAT OUT under
|
|
24937
|
+
* `onUncoveredCondition: 'skip'` — `null` when the scene is checking normally.
|
|
24938
|
+
*
|
|
24939
|
+
* Engine-reported and advisory only: it moves no verdict, no latch and no
|
|
24940
|
+
* hysteresis. It exists so the card can say *"night (IR) — checks paused,
|
|
24941
|
+
* nothing captured in this light"* in the same calm voice as the coverage
|
|
24942
|
+
* line, because the alternative is a scene that silently stops answering
|
|
24943
|
+
* after sunset with nothing anywhere saying why. A skipped check must never
|
|
24944
|
+
* read as a broken one.
|
|
24945
|
+
*/
|
|
24946
|
+
suspendedCondition: SceneConditionSchema.nullable().default(null),
|
|
24696
24947
|
/** Named cause when `verdict === 'unknown'`. */
|
|
24697
24948
|
unavailable: SceneUnavailableSchema.nullable(),
|
|
24698
24949
|
/** Conditions that have at least one comparable reference — the coverage line
|
|
@@ -24736,6 +24987,7 @@ DeviceType.Camera, method(object({ deviceId: number$1() }), SceneMonitorStatusSc
|
|
|
24736
24987
|
minObservationSpacingSec: number$1().int().min(0).max(3600).optional(),
|
|
24737
24988
|
anchorThreshold: number$1().min(0).max(1).optional(),
|
|
24738
24989
|
autoRestore: boolean().optional(),
|
|
24990
|
+
onUncoveredCondition: SceneUncoveredPolicySchema.optional(),
|
|
24739
24991
|
/** `null` clears the vision-model adjudicator. */
|
|
24740
24992
|
confirm: SceneConfirmSchema.nullable().optional()
|
|
24741
24993
|
})
|
|
@@ -27967,6 +28219,12 @@ Object.freeze({
|
|
|
27967
28219
|
addonId: null,
|
|
27968
28220
|
access: "view"
|
|
27969
28221
|
},
|
|
28222
|
+
"llm.resolveModelRef": {
|
|
28223
|
+
capName: "llm",
|
|
28224
|
+
capScope: "system",
|
|
28225
|
+
addonId: null,
|
|
28226
|
+
access: "create"
|
|
28227
|
+
},
|
|
27970
28228
|
"llm.setDefault": {
|
|
27971
28229
|
capName: "llm",
|
|
27972
28230
|
capScope: "system",
|
|
@@ -45675,7 +45933,7 @@ function inferDocMediaType(uriOrName) {
|
|
|
45675
45933
|
for (const [ext, media] of Object.entries(KNOWN_DOC_EXTENSIONS)) if (lower.endsWith(`.${ext}`)) return media;
|
|
45676
45934
|
return "application/octet-stream";
|
|
45677
45935
|
}
|
|
45678
|
-
function basename$
|
|
45936
|
+
function basename$2(uriOrName) {
|
|
45679
45937
|
const parts = uriOrName.split("/");
|
|
45680
45938
|
const last = parts[parts.length - 1];
|
|
45681
45939
|
return last && last.length > 0 ? last : void 0;
|
|
@@ -45705,7 +45963,7 @@ function annotationToSource({ annotation, generateId: generateId3 }) {
|
|
|
45705
45963
|
url: uri,
|
|
45706
45964
|
...fileCitation.file_name != null ? { title: fileCitation.file_name } : {}
|
|
45707
45965
|
};
|
|
45708
|
-
const filename = (_c = fileCitation.file_name) != null ? _c : basename$
|
|
45966
|
+
const filename = (_c = fileCitation.file_name) != null ? _c : basename$2(uri);
|
|
45709
45967
|
const mediaType = inferDocMediaType(uri);
|
|
45710
45968
|
return {
|
|
45711
45969
|
type: "source",
|
|
@@ -45794,7 +46052,7 @@ function builtinToolResultToSources({ block, generateId: generateId3 }) {
|
|
|
45794
46052
|
});
|
|
45795
46053
|
continue;
|
|
45796
46054
|
}
|
|
45797
|
-
const filename = (_h = entry.file_name) != null ? _h : basename$
|
|
46055
|
+
const filename = (_h = entry.file_name) != null ? _h : basename$2(uri);
|
|
45798
46056
|
const mediaType = inferDocMediaType(uri);
|
|
45799
46057
|
sources.push({
|
|
45800
46058
|
type: "source",
|
|
@@ -68783,6 +69041,108 @@ createIdGenerator({
|
|
|
68783
69041
|
size: 24
|
|
68784
69042
|
});
|
|
68785
69043
|
//#endregion
|
|
69044
|
+
//#region src/client/connect-probe.ts
|
|
69045
|
+
/**
|
|
69046
|
+
* "Is this endpoint accepting connections?" — and deliberately nothing else.
|
|
69047
|
+
*
|
|
69048
|
+
* ## Why this exists as its own step
|
|
69049
|
+
*
|
|
69050
|
+
* The `llm` taxonomy has always claimed a CONNECT bound distinct from the
|
|
69051
|
+
* FIRST-TOKEN one, on the grounds that they are different faults with different
|
|
69052
|
+
* remedies. The implementation did not honour that: it armed the 10 s connect
|
|
69053
|
+
* timer around the wait for HTTP RESPONSE HEADERS. On the wire this repo talks
|
|
69054
|
+
* to most — LM Studio / llama-server — the headers are the LAST thing that
|
|
69055
|
+
* happens before the first token: the server accepts the socket, reads the
|
|
69056
|
+
* request, loads the model into the GPU (minutes for qwen3-vl), and only then
|
|
69057
|
+
* writes a status line. So a cold load was reported as
|
|
69058
|
+
* `unavailable: the endpoint did not accept the connection within 10s`, and the
|
|
69059
|
+
* operator was sent to check a base URL that was correct. It cost two live
|
|
69060
|
+
* debugging sessions.
|
|
69061
|
+
*
|
|
69062
|
+
* A TCP handshake is the only thing that answers the connect question without
|
|
69063
|
+
* ambiguity, so that is what this probes: a closed port fails at once with
|
|
69064
|
+
* `ECONNREFUSED`, a black-holed address burns the whole bound, and a listening
|
|
69065
|
+
* endpoint says yes in a millisecond on a LAN — whatever it plans to do next.
|
|
69066
|
+
*
|
|
69067
|
+
* The socket is closed immediately. This is a probe, not the request; the real
|
|
69068
|
+
* call dials its own connection through the library's `fetch` a moment later.
|
|
69069
|
+
* That gap is a theoretical race (the port could shut in between) and a real
|
|
69070
|
+
* one would surface as the ordinary network error it is.
|
|
69071
|
+
*/
|
|
69072
|
+
var defaultConnectImpl = (endpoint) => connect({
|
|
69073
|
+
host: endpoint.host,
|
|
69074
|
+
port: endpoint.port
|
|
69075
|
+
});
|
|
69076
|
+
/**
|
|
69077
|
+
* The TCP endpoint a base URL points at, or `null` when there is not one.
|
|
69078
|
+
*
|
|
69079
|
+
* `null` is "do not probe", never "the endpoint is down": a profile whose URL
|
|
69080
|
+
* this cannot parse must fail on the real request with the real reason, not on
|
|
69081
|
+
* a guess made here.
|
|
69082
|
+
*/
|
|
69083
|
+
function tcpEndpointOf(baseUrl) {
|
|
69084
|
+
let url;
|
|
69085
|
+
try {
|
|
69086
|
+
url = new URL(baseUrl);
|
|
69087
|
+
} catch {
|
|
69088
|
+
return null;
|
|
69089
|
+
}
|
|
69090
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return null;
|
|
69091
|
+
const port = url.port === "" ? url.protocol === "https:" ? 443 : 80 : Number(url.port);
|
|
69092
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) return null;
|
|
69093
|
+
const host = url.hostname.startsWith("[") && url.hostname.endsWith("]") ? url.hostname.slice(1, -1) : url.hostname;
|
|
69094
|
+
return host.length === 0 ? null : {
|
|
69095
|
+
host,
|
|
69096
|
+
port
|
|
69097
|
+
};
|
|
69098
|
+
}
|
|
69099
|
+
/**
|
|
69100
|
+
* Dial, and report which of the three things happened.
|
|
69101
|
+
*
|
|
69102
|
+
* Never rejects — the caller is `LlmClient`, which owes its own callers a
|
|
69103
|
+
* RESULT rather than a throw. `signal` is honoured so that an operator who
|
|
69104
|
+
* closes the page does not leave a socket dialling for the rest of the bound.
|
|
69105
|
+
*/
|
|
69106
|
+
function probeTcpConnect(endpoint, timeoutMs, signal, connectImpl = defaultConnectImpl) {
|
|
69107
|
+
return new Promise((resolve) => {
|
|
69108
|
+
let settled = false;
|
|
69109
|
+
let socket = null;
|
|
69110
|
+
const settle = (outcome) => {
|
|
69111
|
+
if (settled) return;
|
|
69112
|
+
settled = true;
|
|
69113
|
+
clearTimeout(timer);
|
|
69114
|
+
signal.removeEventListener("abort", onAbort);
|
|
69115
|
+
socket?.destroy();
|
|
69116
|
+
resolve(outcome);
|
|
69117
|
+
};
|
|
69118
|
+
const timer = setTimeout(() => settle({ kind: "timeout" }), timeoutMs);
|
|
69119
|
+
timer.unref?.();
|
|
69120
|
+
const onAbort = () => settle({
|
|
69121
|
+
kind: "error",
|
|
69122
|
+
message: "the connection attempt was cancelled"
|
|
69123
|
+
});
|
|
69124
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
69125
|
+
if (signal.aborted) {
|
|
69126
|
+
onAbort();
|
|
69127
|
+
return;
|
|
69128
|
+
}
|
|
69129
|
+
try {
|
|
69130
|
+
socket = connectImpl(endpoint);
|
|
69131
|
+
} catch (error) {
|
|
69132
|
+
settle({
|
|
69133
|
+
kind: "error",
|
|
69134
|
+
message: error instanceof Error ? error.message : String(error)
|
|
69135
|
+
});
|
|
69136
|
+
return;
|
|
69137
|
+
}
|
|
69138
|
+
socket.once("connect", () => settle({ kind: "connected" }));
|
|
69139
|
+
socket.once("error", (error) => settle({
|
|
69140
|
+
kind: "error",
|
|
69141
|
+
message: error.message
|
|
69142
|
+
}));
|
|
69143
|
+
});
|
|
69144
|
+
}
|
|
69145
|
+
//#endregion
|
|
68786
69146
|
//#region src/client/llm-client.ts
|
|
68787
69147
|
/**
|
|
68788
69148
|
* `LlmClient` — the ONE file in this repo allowed to import the LLM library.
|
|
@@ -68847,6 +69207,19 @@ function baseUrlFor$1(profile) {
|
|
|
68847
69207
|
return profile.kind === "openai" ? OPENAI_DEFAULT_BASE_URL$1 : null;
|
|
68848
69208
|
}
|
|
68849
69209
|
/**
|
|
69210
|
+
* Where to knock, for the connect probe.
|
|
69211
|
+
*
|
|
69212
|
+
* Wider than {@link baseUrlFor}: an Anthropic or Google profile with an
|
|
69213
|
+
* explicit `baseUrl` (a LAN proxy, a gateway) is just as probe-able as an
|
|
69214
|
+
* openai-compatible one. Only a profile that relies on a vendor's built-in
|
|
69215
|
+
* endpoint yields `null` — the client never learns that URL, and inventing one
|
|
69216
|
+
* to probe would be probing a different host from the one the call uses.
|
|
69217
|
+
*/
|
|
69218
|
+
function probeEndpointFor(profile) {
|
|
69219
|
+
const baseUrl = profile.baseUrl !== void 0 && profile.baseUrl.length > 0 ? trimSlash(profile.baseUrl) : baseUrlFor$1(profile);
|
|
69220
|
+
return baseUrl === null ? null : tcpEndpointOf(baseUrl);
|
|
69221
|
+
}
|
|
69222
|
+
/**
|
|
68850
69223
|
* Build the provider instance for a profile.
|
|
68851
69224
|
*
|
|
68852
69225
|
* Returns `null` when the profile cannot be served — a missing base URL on an
|
|
@@ -69105,7 +69478,7 @@ function createLlmClient(deps = {}) {
|
|
|
69105
69478
|
if (isAbortLike(error) && !request.signal.aborted) return {
|
|
69106
69479
|
ok: false,
|
|
69107
69480
|
code: "timeout",
|
|
69108
|
-
message: `
|
|
69481
|
+
message: `no answer within ${String(Math.round(timeoutMs / 1e3))}s — a cold model can take minutes to load; warm it or raise the profile's total timeout`
|
|
69109
69482
|
};
|
|
69110
69483
|
const mapped = mapLibraryError(error);
|
|
69111
69484
|
return {
|
|
@@ -69119,41 +69492,91 @@ function createLlmClient(deps = {}) {
|
|
|
69119
69492
|
if (!SUPPORTED_KINDS.has(request.profile.kind)) return { kind: "unsupported" };
|
|
69120
69493
|
const model = modelFor(request.profile);
|
|
69121
69494
|
if (model === null) return { kind: "unsupported" };
|
|
69122
|
-
const
|
|
69123
|
-
|
|
69495
|
+
const endpoint = probeEndpointFor(request.profile);
|
|
69496
|
+
if (endpoint !== null) {
|
|
69497
|
+
const probe = await probeTcpConnect(endpoint, opts.connectTimeoutMs, request.signal, deps.connectImpl ?? defaultConnectImpl);
|
|
69498
|
+
if (probe.kind === "timeout") return {
|
|
69499
|
+
kind: "connect-timeout",
|
|
69500
|
+
timeoutMs: opts.connectTimeoutMs
|
|
69501
|
+
};
|
|
69502
|
+
if (probe.kind === "error") return {
|
|
69503
|
+
kind: "network",
|
|
69504
|
+
message: request.signal.aborted ? "the call was cancelled" : probe.message
|
|
69505
|
+
};
|
|
69506
|
+
opts.onConnected?.();
|
|
69507
|
+
}
|
|
69508
|
+
const firstChunkBound = new AbortController();
|
|
69509
|
+
const onOuterAbort = () => firstChunkBound.abort();
|
|
69124
69510
|
request.signal.addEventListener("abort", onOuterAbort, { once: true });
|
|
69125
|
-
const
|
|
69126
|
-
|
|
69511
|
+
const firstChunkTimer = setTimeout(() => firstChunkBound.abort(), opts.firstTokenTimeoutMs);
|
|
69512
|
+
firstChunkTimer.unref?.();
|
|
69513
|
+
let streamFailure;
|
|
69127
69514
|
try {
|
|
69128
|
-
const
|
|
69515
|
+
const iterator = chunksFrom(streamText({
|
|
69129
69516
|
model,
|
|
69130
69517
|
messages: messagesFor(request),
|
|
69131
69518
|
...instructionsFor(request),
|
|
69132
69519
|
...callSettingsFor(request),
|
|
69133
69520
|
...structuredOutputFor(request),
|
|
69134
|
-
abortSignal: AbortSignal.any([request.signal,
|
|
69135
|
-
|
|
69136
|
-
|
|
69521
|
+
abortSignal: AbortSignal.any([request.signal, firstChunkBound.signal]),
|
|
69522
|
+
onError: ({ error }) => {
|
|
69523
|
+
streamFailure = error;
|
|
69524
|
+
}
|
|
69525
|
+
}))[Symbol.asyncIterator]();
|
|
69526
|
+
const first = await iterator.next();
|
|
69527
|
+
if (streamFailure !== void 0) {
|
|
69528
|
+
const mapped = mapLibraryError(streamFailure);
|
|
69529
|
+
return {
|
|
69530
|
+
kind: "provider-error",
|
|
69531
|
+
code: mapped.code,
|
|
69532
|
+
message: mapped.message
|
|
69533
|
+
};
|
|
69534
|
+
}
|
|
69535
|
+
if (firstChunkBound.signal.aborted) return request.signal.aborted ? {
|
|
69536
|
+
kind: "network",
|
|
69537
|
+
message: "the call was cancelled"
|
|
69538
|
+
} : {
|
|
69539
|
+
kind: "first-token-timeout",
|
|
69540
|
+
timeoutMs: opts.firstTokenTimeoutMs
|
|
69541
|
+
};
|
|
69137
69542
|
return {
|
|
69138
69543
|
kind: "open",
|
|
69139
|
-
chunks:
|
|
69544
|
+
chunks: resumeFrom(first, iterator)
|
|
69140
69545
|
};
|
|
69141
69546
|
} catch (error) {
|
|
69142
69547
|
if (isAbortLike(error) && !request.signal.aborted) return {
|
|
69143
|
-
kind: "
|
|
69144
|
-
timeoutMs: opts.
|
|
69548
|
+
kind: "first-token-timeout",
|
|
69549
|
+
timeoutMs: opts.firstTokenTimeoutMs
|
|
69145
69550
|
};
|
|
69551
|
+
const mapped = mapLibraryError(error);
|
|
69146
69552
|
return {
|
|
69147
|
-
kind: "
|
|
69148
|
-
|
|
69553
|
+
kind: "provider-error",
|
|
69554
|
+
code: mapped.code,
|
|
69555
|
+
message: mapped.message
|
|
69149
69556
|
};
|
|
69150
69557
|
} finally {
|
|
69151
|
-
clearTimeout(
|
|
69558
|
+
clearTimeout(firstChunkTimer);
|
|
69152
69559
|
request.signal.removeEventListener("abort", onOuterAbort);
|
|
69153
69560
|
}
|
|
69154
69561
|
}
|
|
69155
69562
|
};
|
|
69156
69563
|
}
|
|
69564
|
+
/**
|
|
69565
|
+
* Hand back a stream whose first chunk has already been pulled.
|
|
69566
|
+
*
|
|
69567
|
+
* `openStream` has to consume one chunk to know the model started — that is
|
|
69568
|
+
* what its bound measures — and the caller must still receive it. Replaying it
|
|
69569
|
+
* here is what keeps "the first token is the load signal" true for the reader.
|
|
69570
|
+
*/
|
|
69571
|
+
async function* resumeFrom(first, iterator) {
|
|
69572
|
+
if (first.done === true) return;
|
|
69573
|
+
yield first.value;
|
|
69574
|
+
for (;;) {
|
|
69575
|
+
const next = await iterator.next();
|
|
69576
|
+
if (next.done === true) return;
|
|
69577
|
+
yield next.value;
|
|
69578
|
+
}
|
|
69579
|
+
}
|
|
69157
69580
|
async function* chunksFrom(stream) {
|
|
69158
69581
|
for await (const text of stream.textStream) if (text.length > 0) yield {
|
|
69159
69582
|
kind: "token",
|
|
@@ -69499,7 +69922,7 @@ function httpProfileConfigSchema(opts) {
|
|
|
69499
69922
|
min: 500,
|
|
69500
69923
|
default: 1e4,
|
|
69501
69924
|
unit: "ms",
|
|
69502
|
-
description: "
|
|
69925
|
+
description: "The TCP handshake — i.e. \"is the port even open\". A closed port fails at once; only a black hole spends the whole bound."
|
|
69503
69926
|
},
|
|
69504
69927
|
{
|
|
69505
69928
|
type: "number",
|
|
@@ -69508,7 +69931,7 @@ function httpProfileConfigSchema(opts) {
|
|
|
69508
69931
|
min: 1e3,
|
|
69509
69932
|
default: 12e4,
|
|
69510
69933
|
unit: "ms",
|
|
69511
|
-
description: "Accepted but silent. A cold GPU load lives here and can take minutes."
|
|
69934
|
+
description: "Accepted but silent — response headers included, since a model writes them once it has loaded. A cold GPU load lives here and can take minutes. The test chat waits exactly this long."
|
|
69512
69935
|
},
|
|
69513
69936
|
{
|
|
69514
69937
|
type: "number",
|
|
@@ -69836,6 +70259,469 @@ function resolveProfile(profiles, defaults, input) {
|
|
|
69836
70259
|
};
|
|
69837
70260
|
}
|
|
69838
70261
|
//#endregion
|
|
70262
|
+
//#region src/runtime/hf-ref.ts
|
|
70263
|
+
/**
|
|
70264
|
+
* Hugging Face model references — parse, then resolve against the HF API.
|
|
70265
|
+
*
|
|
70266
|
+
* The operator types ONE string and gets a fully-pinned download plan. That is
|
|
70267
|
+
* the whole surface: this is not an HF browser, and it deliberately cannot
|
|
70268
|
+
* discover a model for you — it can only turn a reference you already have
|
|
70269
|
+
* into something the node can fetch and verify.
|
|
70270
|
+
*
|
|
70271
|
+
* ## Why resolution can REFUSE
|
|
70272
|
+
*
|
|
70273
|
+
* A GGUF repo is not one model. `unsloth/Qwen3.6-35B-A3B-GGUF` ships 25
|
|
70274
|
+
* quantizations between 10 GB and 50 GB, and none of them is named `Q4_K_M`
|
|
70275
|
+
* (they are `UD-Q4_K_M`, Unsloth's dynamic quant). Any code that "defaults to
|
|
70276
|
+
* Q4_K_M" would either fail or, worse, pick a neighbouring file and hand the
|
|
70277
|
+
* operator a model they did not ask for after a 20 GB download. So: a repo
|
|
70278
|
+
* with more than one candidate is an ERROR that NAMES the candidates, never a
|
|
70279
|
+
* guess. The only silent pick is the mmproj precision (F16 over F32) — that
|
|
70280
|
+
* choice costs a few hundred MB of projector, not a different model, and the
|
|
70281
|
+
* file it picked is reported back.
|
|
70282
|
+
*
|
|
70283
|
+
* ## The error taxonomy is read from headers, not from the status
|
|
70284
|
+
*
|
|
70285
|
+
* Probed live on 2026-08-15: huggingface.co answers **401** both for a gated
|
|
70286
|
+
* repo and for a repo that does not exist (it refuses to leak whether a
|
|
70287
|
+
* private repo is there). The two are distinguishable only by
|
|
70288
|
+
* `x-error-code: GatedRepo`. Reading the status alone would tell a
|
|
70289
|
+
* typo'd repo name that it needs a token, which is the wrong instruction.
|
|
70290
|
+
*
|
|
70291
|
+
* ## What is verified before a byte is downloaded
|
|
70292
|
+
*
|
|
70293
|
+
* host is huggingface.co · extension is `.gguf` · every file exists in the
|
|
70294
|
+
* tree · the split-GGUF shard set is COMPLETE · the total (main + shards +
|
|
70295
|
+
* mmproj) is under the ceiling · a HEAD confirms the file is reachable with
|
|
70296
|
+
* the credentials at hand and that its size agrees with the tree. The sha256
|
|
70297
|
+
* comes free: HF's LFS `oid` IS the sha256 of the file, and `x-linked-etag`
|
|
70298
|
+
* repeats it on the HEAD.
|
|
70299
|
+
*/
|
|
70300
|
+
/** The only hosts a reference may point at. */
|
|
70301
|
+
var HF_HOSTS = ["huggingface.co", "www.huggingface.co"];
|
|
70302
|
+
var HF_API = "https://huggingface.co/api/models";
|
|
70303
|
+
var HF_RESOLVE = "https://huggingface.co";
|
|
70304
|
+
/** Where an operator puts a Hugging Face token, named in the gated error. */
|
|
70305
|
+
var HF_TOKEN_ENV = "HF_TOKEN";
|
|
70306
|
+
var SEGMENT_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
70307
|
+
function fail(code, message, candidates) {
|
|
70308
|
+
return {
|
|
70309
|
+
code,
|
|
70310
|
+
message,
|
|
70311
|
+
...candidates !== void 0 ? { candidates } : {}
|
|
70312
|
+
};
|
|
70313
|
+
}
|
|
70314
|
+
function badParse(code, message, candidates) {
|
|
70315
|
+
return {
|
|
70316
|
+
ok: false,
|
|
70317
|
+
error: fail(code, message, candidates)
|
|
70318
|
+
};
|
|
70319
|
+
}
|
|
70320
|
+
var EXPECTED = "expected https://huggingface.co/<org>/<repo>/resolve/main/<file>.gguf, or <org>/<repo>/<file>.gguf, or <org>/<repo>[:<QUANT>]";
|
|
70321
|
+
/**
|
|
70322
|
+
* Reference string → a repo/file reference. Pure: no network, no environment.
|
|
70323
|
+
* Every rejection names the form that WAS expected, because the operator is
|
|
70324
|
+
* pasting from a browser and a bare "invalid" tells them nothing.
|
|
70325
|
+
*/
|
|
70326
|
+
function parseHfRef(input) {
|
|
70327
|
+
const raw = input.trim();
|
|
70328
|
+
if (raw === "") return badParse("malformed", `empty model reference — ${EXPECTED}`);
|
|
70329
|
+
return raw.includes("://") ? parseUrlForm(raw) : parseBareForm(raw);
|
|
70330
|
+
}
|
|
70331
|
+
function parseUrlForm(raw) {
|
|
70332
|
+
let url;
|
|
70333
|
+
try {
|
|
70334
|
+
url = new URL(raw);
|
|
70335
|
+
} catch {
|
|
70336
|
+
return badParse("malformed", `not a URL: ${raw} — ${EXPECTED}`);
|
|
70337
|
+
}
|
|
70338
|
+
if (!HF_HOSTS.includes(url.hostname)) return badParse("not-huggingface", `only huggingface.co models can be installed this way; got host "${url.hostname}"`);
|
|
70339
|
+
const parts = url.pathname.split("/").filter((p) => p !== "");
|
|
70340
|
+
const marker = parts.findIndex((p) => p === "resolve" || p === "blob");
|
|
70341
|
+
if (marker !== 2 || parts.length < marker + 3) return badParse("malformed", `unrecognised Hugging Face URL: ${raw} — ${EXPECTED}`);
|
|
70342
|
+
return finishParse(`${String(parts[0])}/${String(parts[1])}`, String(parts[marker + 1]), parts.slice(marker + 2).join("/"), raw);
|
|
70343
|
+
}
|
|
70344
|
+
function parseBareForm(raw) {
|
|
70345
|
+
const [beforeTag, ...tagRest] = raw.split(":");
|
|
70346
|
+
const body = String(beforeTag);
|
|
70347
|
+
if (tagRest.length > 1) return badParse("malformed", `too many ":" in ${raw} — ${EXPECTED}`);
|
|
70348
|
+
const quant = tagRest[0]?.trim();
|
|
70349
|
+
const parts = body.split("/");
|
|
70350
|
+
if (parts.length < 2) return badParse("malformed", `not an <org>/<repo> reference: ${raw} — ${EXPECTED}`);
|
|
70351
|
+
if (parts.some((p) => p === "" || p === "." || p === "..")) return badParse("malformed", `illegal path segment in ${raw} — ${EXPECTED}`);
|
|
70352
|
+
const org = String(parts[0]);
|
|
70353
|
+
const name = String(parts[1]);
|
|
70354
|
+
if (!SEGMENT_RE.test(org) || !SEGMENT_RE.test(name)) return badParse("malformed", `illegal repo name in ${raw} — ${EXPECTED}`);
|
|
70355
|
+
const repo = `${org}/${name}`;
|
|
70356
|
+
if (parts.length === 2) {
|
|
70357
|
+
if (quant !== void 0 && quant === "") return badParse("malformed", `empty quantization tag in ${raw} — ${EXPECTED}`);
|
|
70358
|
+
return {
|
|
70359
|
+
ok: true,
|
|
70360
|
+
ref: {
|
|
70361
|
+
kind: "repo",
|
|
70362
|
+
repo,
|
|
70363
|
+
revision: "main",
|
|
70364
|
+
...quant !== void 0 ? { quant } : {}
|
|
70365
|
+
}
|
|
70366
|
+
};
|
|
70367
|
+
}
|
|
70368
|
+
if (quant !== void 0) return badParse("malformed", `a quantization tag cannot follow an explicit file: ${raw}`);
|
|
70369
|
+
return finishParse(repo, "main", parts.slice(2).join("/"), raw);
|
|
70370
|
+
}
|
|
70371
|
+
function finishParse(repo, revision, filePath, raw) {
|
|
70372
|
+
if (filePath.split("/").some((p) => p === "" || p === "." || p === "..")) return badParse("malformed", `illegal path segment in ${raw} — ${EXPECTED}`);
|
|
70373
|
+
if (!filePath.toLowerCase().endsWith(".gguf")) return badParse("not-gguf", `the managed local runtime loads GGUF only; "${filePath}" is not a .gguf file`);
|
|
70374
|
+
return {
|
|
70375
|
+
ok: true,
|
|
70376
|
+
ref: {
|
|
70377
|
+
kind: "file",
|
|
70378
|
+
repo,
|
|
70379
|
+
revision,
|
|
70380
|
+
filePath
|
|
70381
|
+
}
|
|
70382
|
+
};
|
|
70383
|
+
}
|
|
70384
|
+
/** `-00001-of-00002` — llama.cpp's split-GGUF naming. */
|
|
70385
|
+
var SHARD_RE = /^(.*)-(\d{5})-of-(\d{5})$/;
|
|
70386
|
+
/**
|
|
70387
|
+
* One `-`-delimited segment that is a quantization, e.g. `Q4_K_M`, `IQ2_XXS`,
|
|
70388
|
+
* `BF16`, `fp16`. The `FP` spellings are not cosmetic: `Qwen/*-GGUF` names its
|
|
70389
|
+
* unquantized file `…-fp16.gguf`, and a tag list that cannot name it offers
|
|
70390
|
+
* the operator a suggestion that does not parse.
|
|
70391
|
+
*/
|
|
70392
|
+
var QUANT_RE = /^(?:I?Q\d[A-Z0-9_]*|TQ\d_\d|BF16|FP?16|FP?32|FP8|MXFP4(?:_MOE)?)$/i;
|
|
70393
|
+
/** Shard coordinates of a split GGUF filename, or `null` when unsharded. */
|
|
70394
|
+
function shardInfoOf(filename) {
|
|
70395
|
+
const m = SHARD_RE.exec(stripGguf(filename));
|
|
70396
|
+
if (m === null) return null;
|
|
70397
|
+
return {
|
|
70398
|
+
stem: String(m[1]),
|
|
70399
|
+
index: Number(m[2]),
|
|
70400
|
+
total: Number(m[3])
|
|
70401
|
+
};
|
|
70402
|
+
}
|
|
70403
|
+
function stripGguf(filename) {
|
|
70404
|
+
return filename.replace(/\.gguf$/i, "");
|
|
70405
|
+
}
|
|
70406
|
+
/**
|
|
70407
|
+
* The quantization tag of a GGUF filename, uppercased, `UD-` prefix kept —
|
|
70408
|
+
* `''` when the name carries no recognisable tag. Shard coordinates are
|
|
70409
|
+
* stripped first so `X-BF16-00001-of-00002.gguf` reads as `BF16`.
|
|
70410
|
+
*/
|
|
70411
|
+
function quantizationOf(filename) {
|
|
70412
|
+
const segments = (shardInfoOf(filename)?.stem ?? stripGguf(filename)).split("-");
|
|
70413
|
+
for (let i = segments.length - 1; i >= 0; i--) {
|
|
70414
|
+
const seg = String(segments[i]);
|
|
70415
|
+
if (!QUANT_RE.test(seg)) continue;
|
|
70416
|
+
return (i > 0 ? String(segments[i - 1]) : "").toUpperCase() === "UD" ? `UD-${seg.toUpperCase()}` : seg.toUpperCase();
|
|
70417
|
+
}
|
|
70418
|
+
return "";
|
|
70419
|
+
}
|
|
70420
|
+
function isMmproj(filePath) {
|
|
70421
|
+
return basename$1(filePath).toLowerCase().startsWith("mmproj");
|
|
70422
|
+
}
|
|
70423
|
+
function basename$1(filePath) {
|
|
70424
|
+
return filePath.slice(filePath.lastIndexOf("/") + 1);
|
|
70425
|
+
}
|
|
70426
|
+
function dirname(filePath) {
|
|
70427
|
+
const i = filePath.lastIndexOf("/");
|
|
70428
|
+
return i < 0 ? "" : filePath.slice(0, i);
|
|
70429
|
+
}
|
|
70430
|
+
function headersFor(token) {
|
|
70431
|
+
return {
|
|
70432
|
+
"User-Agent": "CamStack/1.0",
|
|
70433
|
+
...token !== void 0 && token !== "" ? { Authorization: `Bearer ${token}` } : {}
|
|
70434
|
+
};
|
|
70435
|
+
}
|
|
70436
|
+
/** HF's 401-for-everything is only decodable through `x-error-code`. */
|
|
70437
|
+
function authError(response, repo) {
|
|
70438
|
+
const code = response.headers.get("x-error-code") ?? "";
|
|
70439
|
+
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.`);
|
|
70440
|
+
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.`);
|
|
70441
|
+
}
|
|
70442
|
+
async function readTree(ref, fetchFn, token) {
|
|
70443
|
+
const url = `${HF_API}/${ref.repo}/tree/${ref.revision}?recursive=1`;
|
|
70444
|
+
let response;
|
|
70445
|
+
try {
|
|
70446
|
+
response = await fetchFn(url, {
|
|
70447
|
+
method: "GET",
|
|
70448
|
+
headers: headersFor(token)
|
|
70449
|
+
});
|
|
70450
|
+
} catch (err) {
|
|
70451
|
+
return {
|
|
70452
|
+
ok: false,
|
|
70453
|
+
error: fail("network", `could not reach huggingface.co: ${message(err)}`)
|
|
70454
|
+
};
|
|
70455
|
+
}
|
|
70456
|
+
if (response.status === 401 || response.status === 403) return {
|
|
70457
|
+
ok: false,
|
|
70458
|
+
error: authError(response, ref.repo)
|
|
70459
|
+
};
|
|
70460
|
+
if (response.status === 404) return {
|
|
70461
|
+
ok: false,
|
|
70462
|
+
error: fail("repo-not-found", `${ref.repo} has no revision "${ref.revision}"`)
|
|
70463
|
+
};
|
|
70464
|
+
if (!response.ok) return {
|
|
70465
|
+
ok: false,
|
|
70466
|
+
error: fail("network", `huggingface.co answered ${String(response.status)} for ${ref.repo}`)
|
|
70467
|
+
};
|
|
70468
|
+
let body;
|
|
70469
|
+
try {
|
|
70470
|
+
body = await response.json();
|
|
70471
|
+
} catch (err) {
|
|
70472
|
+
return {
|
|
70473
|
+
ok: false,
|
|
70474
|
+
error: fail("network", `unreadable tree for ${ref.repo}: ${message(err)}`)
|
|
70475
|
+
};
|
|
70476
|
+
}
|
|
70477
|
+
if (!Array.isArray(body)) return {
|
|
70478
|
+
ok: false,
|
|
70479
|
+
error: fail("network", `unexpected tree payload for ${ref.repo}`)
|
|
70480
|
+
};
|
|
70481
|
+
return {
|
|
70482
|
+
ok: true,
|
|
70483
|
+
files: body.map(toTreeEntry).filter((e) => e !== null)
|
|
70484
|
+
};
|
|
70485
|
+
}
|
|
70486
|
+
function toTreeEntry(raw) {
|
|
70487
|
+
if (typeof raw !== "object" || raw === null) return null;
|
|
70488
|
+
const record = { ...raw };
|
|
70489
|
+
if (record["type"] !== "file") return null;
|
|
70490
|
+
const filePath = record["path"];
|
|
70491
|
+
if (typeof filePath !== "string" || !filePath.toLowerCase().endsWith(".gguf")) return null;
|
|
70492
|
+
const lfs = typeof record["lfs"] === "object" && record["lfs"] !== null ? { ...record["lfs"] } : {};
|
|
70493
|
+
const lfsSize = lfs["size"];
|
|
70494
|
+
const oid = lfs["oid"];
|
|
70495
|
+
const plainSize = record["size"];
|
|
70496
|
+
return {
|
|
70497
|
+
path: filePath,
|
|
70498
|
+
sizeBytes: typeof lfsSize === "number" ? lfsSize : typeof plainSize === "number" ? plainSize : 0,
|
|
70499
|
+
...typeof oid === "string" && oid.length === 64 ? { sha256: oid } : {}
|
|
70500
|
+
};
|
|
70501
|
+
}
|
|
70502
|
+
function message(err) {
|
|
70503
|
+
return err instanceof Error ? err.message : String(err);
|
|
70504
|
+
}
|
|
70505
|
+
/** Files that can be THE model: not a projector, not a follow-on shard. */
|
|
70506
|
+
function modelCandidates(files) {
|
|
70507
|
+
return files.filter((f) => {
|
|
70508
|
+
if (isMmproj(f.path)) return false;
|
|
70509
|
+
const shard = shardInfoOf(basename$1(f.path));
|
|
70510
|
+
return shard === null || shard.index === 1;
|
|
70511
|
+
});
|
|
70512
|
+
}
|
|
70513
|
+
function labelFor(file) {
|
|
70514
|
+
const quant = quantizationOf(basename$1(file.path));
|
|
70515
|
+
return quant === "" ? basename$1(file.path) : quant;
|
|
70516
|
+
}
|
|
70517
|
+
function selectMain(ref, files) {
|
|
70518
|
+
const candidates = modelCandidates(files);
|
|
70519
|
+
if (ref.kind === "file") {
|
|
70520
|
+
const wanted = ref.filePath.toLowerCase();
|
|
70521
|
+
const hit = files.find((f) => f.path.toLowerCase() === wanted);
|
|
70522
|
+
if (hit === void 0) return {
|
|
70523
|
+
ok: false,
|
|
70524
|
+
error: fail("file-not-found", `${ref.repo} has no file "${ref.filePath}" at revision ${ref.revision}`, candidates.map(labelFor))
|
|
70525
|
+
};
|
|
70526
|
+
return {
|
|
70527
|
+
ok: true,
|
|
70528
|
+
file: hit
|
|
70529
|
+
};
|
|
70530
|
+
}
|
|
70531
|
+
if (candidates.length === 0) return {
|
|
70532
|
+
ok: false,
|
|
70533
|
+
error: fail("not-gguf", `${ref.repo} publishes no GGUF weights (only projectors or no GGUF at all)`)
|
|
70534
|
+
};
|
|
70535
|
+
if (ref.quant !== void 0) {
|
|
70536
|
+
const wanted = ref.quant.toUpperCase();
|
|
70537
|
+
const wantedFile = stripGguf(ref.quant).toUpperCase();
|
|
70538
|
+
const matches = candidates.filter((f) => quantizationOf(basename$1(f.path)) === wanted || stripGguf(basename$1(f.path)).toUpperCase() === wantedFile);
|
|
70539
|
+
if (matches.length === 0) return {
|
|
70540
|
+
ok: false,
|
|
70541
|
+
error: fail("file-not-found", `${ref.repo} has no "${ref.quant}" quantization. Available: ${candidates.map(labelFor).join(", ")}`, dedupe(candidates.map(labelFor)))
|
|
70542
|
+
};
|
|
70543
|
+
const only = matches[0];
|
|
70544
|
+
if (matches.length > 1 || only === void 0) return {
|
|
70545
|
+
ok: false,
|
|
70546
|
+
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)))
|
|
70547
|
+
};
|
|
70548
|
+
return {
|
|
70549
|
+
ok: true,
|
|
70550
|
+
file: only
|
|
70551
|
+
};
|
|
70552
|
+
}
|
|
70553
|
+
const solo = candidates[0];
|
|
70554
|
+
if (candidates.length > 1 || solo === void 0) {
|
|
70555
|
+
const tags = dedupe(candidates.map(labelFor));
|
|
70556
|
+
return {
|
|
70557
|
+
ok: false,
|
|
70558
|
+
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)
|
|
70559
|
+
};
|
|
70560
|
+
}
|
|
70561
|
+
return {
|
|
70562
|
+
ok: true,
|
|
70563
|
+
file: solo
|
|
70564
|
+
};
|
|
70565
|
+
}
|
|
70566
|
+
function dedupe(values) {
|
|
70567
|
+
return [...new Set(values)];
|
|
70568
|
+
}
|
|
70569
|
+
/** Shards 2..N of `main`, or an error naming the first one that is missing. */
|
|
70570
|
+
function collectShards(main, files) {
|
|
70571
|
+
const shard = shardInfoOf(basename$1(main.path));
|
|
70572
|
+
if (shard === null || shard.total <= 1) return {
|
|
70573
|
+
ok: true,
|
|
70574
|
+
shards: []
|
|
70575
|
+
};
|
|
70576
|
+
const dir = dirname(main.path);
|
|
70577
|
+
const out = [];
|
|
70578
|
+
for (let i = 2; i <= shard.total; i++) {
|
|
70579
|
+
const wanted = `${shard.stem}-${String(i).padStart(5, "0")}-of-${String(shard.total).padStart(5, "0")}.gguf`;
|
|
70580
|
+
const full = dir === "" ? wanted : `${dir}/${wanted}`;
|
|
70581
|
+
const hit = files.find((f) => f.path === full);
|
|
70582
|
+
if (hit === void 0) return {
|
|
70583
|
+
ok: false,
|
|
70584
|
+
error: fail("incomplete-shards", `split GGUF is incomplete: ${wanted} is missing from the repo (llama.cpp needs all ${String(shard.total)} shards)`)
|
|
70585
|
+
};
|
|
70586
|
+
out.push(hit);
|
|
70587
|
+
}
|
|
70588
|
+
return {
|
|
70589
|
+
ok: true,
|
|
70590
|
+
shards: out
|
|
70591
|
+
};
|
|
70592
|
+
}
|
|
70593
|
+
/** F16 over BF16 over F32 over whatever came first — reported, never hidden. */
|
|
70594
|
+
var MMPROJ_PREFERENCE = [
|
|
70595
|
+
"F16",
|
|
70596
|
+
"BF16",
|
|
70597
|
+
"F32"
|
|
70598
|
+
];
|
|
70599
|
+
function selectMmproj(files) {
|
|
70600
|
+
const projectors = files.filter((f) => isMmproj(f.path));
|
|
70601
|
+
if (projectors.length === 0) return null;
|
|
70602
|
+
for (const want of MMPROJ_PREFERENCE) {
|
|
70603
|
+
const hit = projectors.find((f) => quantizationOf(basename$1(f.path)) === want);
|
|
70604
|
+
if (hit !== void 0) return hit;
|
|
70605
|
+
}
|
|
70606
|
+
return projectors[0] ?? null;
|
|
70607
|
+
}
|
|
70608
|
+
function resolveUrl(repo, revision, filePath) {
|
|
70609
|
+
return `${HF_RESOLVE}/${repo}/resolve/${revision}/${filePath}`;
|
|
70610
|
+
}
|
|
70611
|
+
async function verifyHead(url, repo, declaredBytes, fetchFn, token) {
|
|
70612
|
+
let response;
|
|
70613
|
+
try {
|
|
70614
|
+
response = await fetchFn(url, {
|
|
70615
|
+
method: "HEAD",
|
|
70616
|
+
redirect: "manual",
|
|
70617
|
+
headers: headersFor(token)
|
|
70618
|
+
});
|
|
70619
|
+
} catch (err) {
|
|
70620
|
+
return {
|
|
70621
|
+
ok: false,
|
|
70622
|
+
error: fail("network", `HEAD ${url} failed: ${message(err)}`)
|
|
70623
|
+
};
|
|
70624
|
+
}
|
|
70625
|
+
if (response.status === 401 || response.status === 403) return {
|
|
70626
|
+
ok: false,
|
|
70627
|
+
error: authError(response, repo)
|
|
70628
|
+
};
|
|
70629
|
+
if (response.status === 404) return {
|
|
70630
|
+
ok: false,
|
|
70631
|
+
error: fail("file-not-found", `${url} is gone (404)`)
|
|
70632
|
+
};
|
|
70633
|
+
if (response.status >= 400) return {
|
|
70634
|
+
ok: false,
|
|
70635
|
+
error: fail("network", `HEAD ${url} answered ${String(response.status)}`)
|
|
70636
|
+
};
|
|
70637
|
+
const linked = response.headers.get("x-linked-size") ?? response.headers.get("content-length");
|
|
70638
|
+
const headBytes = linked === null ? void 0 : Number(linked);
|
|
70639
|
+
if (headBytes !== void 0 && Number.isFinite(headBytes) && headBytes !== declaredBytes) return {
|
|
70640
|
+
ok: false,
|
|
70641
|
+
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`)
|
|
70642
|
+
};
|
|
70643
|
+
const etag = response.headers.get("x-linked-etag")?.replace(/"/g, "");
|
|
70644
|
+
return {
|
|
70645
|
+
ok: true,
|
|
70646
|
+
...etag !== void 0 && etag.length === 64 ? { sha256: etag } : {}
|
|
70647
|
+
};
|
|
70648
|
+
}
|
|
70649
|
+
function toResolved(repo, revision, entry) {
|
|
70650
|
+
return {
|
|
70651
|
+
url: resolveUrl(repo, revision, entry.path),
|
|
70652
|
+
filename: basename$1(entry.path),
|
|
70653
|
+
sizeBytes: entry.sizeBytes,
|
|
70654
|
+
...entry.sha256 !== void 0 ? { sha256: entry.sha256 } : {}
|
|
70655
|
+
};
|
|
70656
|
+
}
|
|
70657
|
+
function gb$1(bytes) {
|
|
70658
|
+
return `${(bytes / 1e9).toFixed(1)} GB`;
|
|
70659
|
+
}
|
|
70660
|
+
/** Reference → a pinned, size-checked, HEAD-verified download plan. */
|
|
70661
|
+
async function resolveHfRef(ref, deps) {
|
|
70662
|
+
const fetchFn = deps.fetchFn ?? fetch;
|
|
70663
|
+
const maxBytes = deps.maxBytes ?? 21474836480;
|
|
70664
|
+
const tree = await readTree(ref, fetchFn, deps.token);
|
|
70665
|
+
if (!tree.ok) return {
|
|
70666
|
+
ok: false,
|
|
70667
|
+
error: tree.error
|
|
70668
|
+
};
|
|
70669
|
+
const picked = selectMain(ref, tree.files);
|
|
70670
|
+
if (!picked.ok) return {
|
|
70671
|
+
ok: false,
|
|
70672
|
+
error: picked.error
|
|
70673
|
+
};
|
|
70674
|
+
const main = picked.file;
|
|
70675
|
+
const shards = collectShards(main, tree.files);
|
|
70676
|
+
if (!shards.ok) return {
|
|
70677
|
+
ok: false,
|
|
70678
|
+
error: shards.error
|
|
70679
|
+
};
|
|
70680
|
+
const projector = isMmproj(main.path) ? null : selectMmproj(tree.files);
|
|
70681
|
+
const extraEntries = [...shards.shards, ...projector === null ? [] : [projector]];
|
|
70682
|
+
const totalBytes = [main, ...extraEntries].reduce((sum, f) => sum + f.sizeBytes, 0);
|
|
70683
|
+
if (totalBytes > maxBytes) return {
|
|
70684
|
+
ok: false,
|
|
70685
|
+
error: {
|
|
70686
|
+
...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.`),
|
|
70687
|
+
requiredBytes: totalBytes
|
|
70688
|
+
}
|
|
70689
|
+
};
|
|
70690
|
+
const head = await verifyHead(resolveUrl(ref.repo, ref.revision, main.path), ref.repo, main.sizeBytes, fetchFn, deps.token);
|
|
70691
|
+
if (!head.ok) return {
|
|
70692
|
+
ok: false,
|
|
70693
|
+
error: head.error
|
|
70694
|
+
};
|
|
70695
|
+
const mainResolved = toResolved(ref.repo, ref.revision, {
|
|
70696
|
+
...main,
|
|
70697
|
+
...main.sha256 === void 0 && head.sha256 !== void 0 ? { sha256: head.sha256 } : {}
|
|
70698
|
+
});
|
|
70699
|
+
const quantization = quantizationOf(mainResolved.filename);
|
|
70700
|
+
const repoName = ref.repo.slice(ref.repo.indexOf("/") + 1);
|
|
70701
|
+
return {
|
|
70702
|
+
ok: true,
|
|
70703
|
+
resolution: {
|
|
70704
|
+
repo: ref.repo,
|
|
70705
|
+
revision: ref.revision,
|
|
70706
|
+
label: quantization === "" ? repoName : `${repoName} · ${quantization}`,
|
|
70707
|
+
quantization,
|
|
70708
|
+
purpose: projector === null ? "text" : "vision",
|
|
70709
|
+
main: mainResolved,
|
|
70710
|
+
extras: extraEntries.map((e) => toResolved(ref.repo, ref.revision, e)),
|
|
70711
|
+
totalBytes
|
|
70712
|
+
}
|
|
70713
|
+
};
|
|
70714
|
+
}
|
|
70715
|
+
/** `parseHfRef` then {@link resolveHfRef} — the form the cap method calls. */
|
|
70716
|
+
async function resolveHfReference(input, deps) {
|
|
70717
|
+
const parsed = parseHfRef(input);
|
|
70718
|
+
if (!parsed.ok) return {
|
|
70719
|
+
ok: false,
|
|
70720
|
+
error: parsed.error
|
|
70721
|
+
};
|
|
70722
|
+
return resolveHfRef(parsed.ref, deps);
|
|
70723
|
+
}
|
|
70724
|
+
//#endregion
|
|
69839
70725
|
//#region src/secrets.ts
|
|
69840
70726
|
/** Same marker as addon-notifiers/src/secrets.ts — UI contract. */
|
|
69841
70727
|
var REDACTED_MARKER = "__redacted__";
|
|
@@ -70059,6 +70945,64 @@ function createLlmProvider(deps) {
|
|
|
70059
70945
|
}));
|
|
70060
70946
|
},
|
|
70061
70947
|
listNodeModels: async ({ nodeId }) => requireRuntime(deps.runtime).listLocalModels(nodeId),
|
|
70948
|
+
/**
|
|
70949
|
+
* Hugging Face reference → a pinned `ManagedModelRef`, on the HUB.
|
|
70950
|
+
*
|
|
70951
|
+
* Never throws: a refusal ("this repo has 24 quantizations", "this is
|
|
70952
|
+
* gated", "23 GB is over the ceiling") is an ANSWER the operator has to
|
|
70953
|
+
* read and act on, and turning it into a tRPC error would reduce all of
|
|
70954
|
+
* them to a red toast with no candidate list and no override.
|
|
70955
|
+
*/
|
|
70956
|
+
resolveModelRef: async ({ ref, maxBytes }) => {
|
|
70957
|
+
const token = deps.hfToken?.();
|
|
70958
|
+
const outcome = await resolveHfReference(ref, {
|
|
70959
|
+
...maxBytes !== void 0 ? { maxBytes } : {},
|
|
70960
|
+
...token !== void 0 && token !== "" ? { token } : {}
|
|
70961
|
+
});
|
|
70962
|
+
if (!outcome.ok) {
|
|
70963
|
+
deps.logger?.info("llm model reference refused", { meta: {
|
|
70964
|
+
ref,
|
|
70965
|
+
code: outcome.error.code
|
|
70966
|
+
} });
|
|
70967
|
+
return {
|
|
70968
|
+
ok: false,
|
|
70969
|
+
code: outcome.error.code,
|
|
70970
|
+
message: outcome.error.message,
|
|
70971
|
+
...outcome.error.candidates !== void 0 ? { candidates: [...outcome.error.candidates] } : {},
|
|
70972
|
+
...outcome.error.requiredBytes !== void 0 ? { requiredBytes: outcome.error.requiredBytes } : {}
|
|
70973
|
+
};
|
|
70974
|
+
}
|
|
70975
|
+
const { resolution } = outcome;
|
|
70976
|
+
deps.logger?.info("llm model reference resolved", { meta: {
|
|
70977
|
+
ref,
|
|
70978
|
+
repo: resolution.repo,
|
|
70979
|
+
quantization: resolution.quantization,
|
|
70980
|
+
purpose: resolution.purpose,
|
|
70981
|
+
totalBytes: resolution.totalBytes
|
|
70982
|
+
} });
|
|
70983
|
+
return {
|
|
70984
|
+
ok: true,
|
|
70985
|
+
model: {
|
|
70986
|
+
kind: "url",
|
|
70987
|
+
url: resolution.main.url,
|
|
70988
|
+
...resolution.main.sha256 !== void 0 ? { sha256: resolution.main.sha256 } : {},
|
|
70989
|
+
label: resolution.label,
|
|
70990
|
+
sizeBytes: resolution.main.sizeBytes,
|
|
70991
|
+
extraFiles: resolution.extras.map((e) => ({
|
|
70992
|
+
url: e.url,
|
|
70993
|
+
filename: e.filename,
|
|
70994
|
+
sizeBytes: e.sizeBytes,
|
|
70995
|
+
...e.sha256 !== void 0 ? { sha256: e.sha256 } : {}
|
|
70996
|
+
}))
|
|
70997
|
+
},
|
|
70998
|
+
label: resolution.label,
|
|
70999
|
+
repo: resolution.repo,
|
|
71000
|
+
quantization: resolution.quantization,
|
|
71001
|
+
purpose: resolution.purpose,
|
|
71002
|
+
totalBytes: resolution.totalBytes,
|
|
71003
|
+
extraFilenames: resolution.extras.map((e) => e.filename)
|
|
71004
|
+
};
|
|
71005
|
+
},
|
|
70062
71006
|
installModel: async ({ nodeId, model }) => {
|
|
70063
71007
|
const runtime = requireRuntime(deps.runtime);
|
|
70064
71008
|
try {
|
|
@@ -70092,13 +71036,29 @@ function createLlmProvider(deps) {
|
|
|
70092
71036
|
//#endregion
|
|
70093
71037
|
//#region src/runtime/llm-model-catalog.ts
|
|
70094
71038
|
/**
|
|
70095
|
-
* Curated managed-model catalog (operator decision #4)
|
|
70096
|
-
*
|
|
70097
|
-
*
|
|
70098
|
-
*
|
|
70099
|
-
*
|
|
70100
|
-
*
|
|
70101
|
-
*
|
|
71039
|
+
* Curated managed-model catalog (operator decision #4). Each entry carries BOTH
|
|
71040
|
+
* the LLM-facing picker view (`meta`) and the REUSED download-plane
|
|
71041
|
+
* `ModelCatalogEntry` (`entry`) so GGUFs ride `ensureModel` +
|
|
71042
|
+
* `model-distributor` untouched — no bespoke fetcher (spec §4.2).
|
|
71043
|
+
* Digests/sizes pinned via scratchpad/pin-llm-models.mjs (HF LFS `lfs.oid`,
|
|
71044
|
+
* which IS the file's sha256).
|
|
71045
|
+
*
|
|
71046
|
+
* ## Two tiers, and `minRamBytes` is what separates them
|
|
71047
|
+
*
|
|
71048
|
+
* The first three entries are sized to the WEAKEST runtime node (the N100
|
|
71049
|
+
* agent): 1-4 GB, Q4. `QWEN36_35B` is not — it is 23 GB and only a big node
|
|
71050
|
+
* can hold it. The catalog does not refuse to show it; `minRamBytes` is the
|
|
71051
|
+
* guidance, and the picker prints the size. Keeping the tiers in one list is
|
|
71052
|
+
* deliberate: an operator with a 64 GB box should not have to discover the
|
|
71053
|
+
* free-text field to run something real.
|
|
71054
|
+
*
|
|
71055
|
+
* ## This list is no longer the boundary of what can run
|
|
71056
|
+
*
|
|
71057
|
+
* Anything on Hugging Face is installable through `llm.resolveModelRef` +
|
|
71058
|
+
* `installModel` without a code change ({@link ./hf-ref.ts}). An entry here
|
|
71059
|
+
* buys exactly two things over typing the reference: a pinned digest nobody
|
|
71060
|
+
* has to re-verify, and a `contextSizeDefault`/`minRamBytes` somebody checked.
|
|
71061
|
+
* Add one only when both are true.
|
|
70102
71062
|
*/
|
|
70103
71063
|
var GIB = 1024 * 1024 * 1024;
|
|
70104
71064
|
function mb(bytes) {
|
|
@@ -70150,30 +71110,117 @@ var LLAMA = textEntry({
|
|
|
70150
71110
|
var SMOLVLM_MODEL_BYTES = 1112602656;
|
|
70151
71111
|
var SMOLVLM_MMPROJ_BYTES = 872303680;
|
|
70152
71112
|
var SMOLVLM_MMPROJ_URL = "https://huggingface.co/ggml-org/SmolVLM2-2.2B-Instruct-GGUF/resolve/main/mmproj-SmolVLM2-2.2B-Instruct-f16.gguf";
|
|
71113
|
+
var SMOLVLM = {
|
|
71114
|
+
meta: {
|
|
71115
|
+
id: "llm-smolvlm2-2.2b-instruct-q4",
|
|
71116
|
+
label: "SmolVLM2 2.2B Instruct (vision)",
|
|
71117
|
+
family: "smolvlm2",
|
|
71118
|
+
purpose: "vision",
|
|
71119
|
+
url: "https://huggingface.co/ggml-org/SmolVLM2-2.2B-Instruct-GGUF/resolve/main/SmolVLM2-2.2B-Instruct-Q4_K_M.gguf",
|
|
71120
|
+
sha256: "0cf76814555b8665149075b74ab6b5c1d428ea1d3d01c1918c12012e8d7c9f58",
|
|
71121
|
+
sizeBytes: SMOLVLM_MODEL_BYTES,
|
|
71122
|
+
quantization: "Q4_K_M",
|
|
71123
|
+
minRamBytes: 4 * GIB,
|
|
71124
|
+
contextSizeDefault: 4096,
|
|
71125
|
+
mmprojUrl: SMOLVLM_MMPROJ_URL
|
|
71126
|
+
},
|
|
71127
|
+
entry: {
|
|
71128
|
+
id: "llm-smolvlm2-2.2b-instruct-q4",
|
|
71129
|
+
name: "SmolVLM2 2.2B Instruct (vision)",
|
|
71130
|
+
description: "smolvlm2 · Q4_K_M · +mmproj",
|
|
71131
|
+
formats: { gguf: {
|
|
71132
|
+
url: "https://huggingface.co/ggml-org/SmolVLM2-2.2B-Instruct-GGUF/resolve/main/SmolVLM2-2.2B-Instruct-Q4_K_M.gguf",
|
|
71133
|
+
sizeMB: mb(SMOLVLM_MODEL_BYTES)
|
|
71134
|
+
} },
|
|
71135
|
+
inputSize: {
|
|
71136
|
+
width: 0,
|
|
71137
|
+
height: 0
|
|
71138
|
+
},
|
|
71139
|
+
labels: [],
|
|
71140
|
+
extraFiles: [{
|
|
71141
|
+
url: SMOLVLM_MMPROJ_URL,
|
|
71142
|
+
filename: "mmproj-SmolVLM2-2.2B-Instruct-f16.gguf",
|
|
71143
|
+
sizeMB: mb(SMOLVLM_MMPROJ_BYTES)
|
|
71144
|
+
}]
|
|
71145
|
+
}
|
|
71146
|
+
};
|
|
71147
|
+
var QWEN3VL2B_MODEL_BYTES = 1107410624;
|
|
71148
|
+
var QWEN3VL2B_MMPROJ_BYTES = 819395232;
|
|
71149
|
+
var QWEN3VL2B_BASE = "https://huggingface.co/unsloth/Qwen3-VL-2B-Instruct-GGUF/resolve/main";
|
|
71150
|
+
var QWEN3VL2B_MMPROJ_URL = `${QWEN3VL2B_BASE}/mmproj-F16.gguf`;
|
|
71151
|
+
var QWEN3VL2B_URL = `${QWEN3VL2B_BASE}/Qwen3-VL-2B-Instruct-Q4_K_M.gguf`;
|
|
71152
|
+
/**
|
|
71153
|
+
* The light vision tier the operator asked for by weight class (~2 GB all in):
|
|
71154
|
+
* same Qwen3-VL family as the LM Studio 8B profile already in daily use, so
|
|
71155
|
+
* prompts and behaviour carry over — at a tenth of the 35B's disk and a RAM
|
|
71156
|
+
* floor a hub-adjacent node can always afford. This is the sensible default
|
|
71157
|
+
* for the NC confirm gates and summary judges.
|
|
71158
|
+
*/
|
|
71159
|
+
var QWEN3VL_2B = {
|
|
71160
|
+
meta: {
|
|
71161
|
+
id: "llm-qwen3-vl-2b-instruct-q4",
|
|
71162
|
+
label: "Qwen3-VL 2B Instruct (vision, light)",
|
|
71163
|
+
family: "qwen3-vl",
|
|
71164
|
+
purpose: "vision",
|
|
71165
|
+
url: QWEN3VL2B_URL,
|
|
71166
|
+
sha256: "858fcf2a39dc73b26dd86592cb0a5f949b59d1edb365d1dea98e46b02e955e56",
|
|
71167
|
+
sizeBytes: QWEN3VL2B_MODEL_BYTES,
|
|
71168
|
+
quantization: "Q4_K_M",
|
|
71169
|
+
minRamBytes: 3 * GIB,
|
|
71170
|
+
contextSizeDefault: 8192,
|
|
71171
|
+
mmprojUrl: QWEN3VL2B_MMPROJ_URL
|
|
71172
|
+
},
|
|
71173
|
+
entry: {
|
|
71174
|
+
id: "llm-qwen3-vl-2b-instruct-q4",
|
|
71175
|
+
name: "Qwen3-VL 2B Instruct (vision, light)",
|
|
71176
|
+
description: "qwen3-vl · Q4_K_M · +mmproj",
|
|
71177
|
+
formats: { gguf: {
|
|
71178
|
+
url: QWEN3VL2B_URL,
|
|
71179
|
+
sizeMB: mb(QWEN3VL2B_MODEL_BYTES)
|
|
71180
|
+
} },
|
|
71181
|
+
inputSize: {
|
|
71182
|
+
width: 0,
|
|
71183
|
+
height: 0
|
|
71184
|
+
},
|
|
71185
|
+
labels: [],
|
|
71186
|
+
extraFiles: [{
|
|
71187
|
+
url: QWEN3VL2B_MMPROJ_URL,
|
|
71188
|
+
filename: "mmproj-F16.gguf",
|
|
71189
|
+
sizeMB: mb(QWEN3VL2B_MMPROJ_BYTES)
|
|
71190
|
+
}]
|
|
71191
|
+
}
|
|
71192
|
+
};
|
|
71193
|
+
var QWEN36_MODEL_BYTES = 22134528992;
|
|
71194
|
+
var QWEN36_MMPROJ_BYTES = 899283680;
|
|
71195
|
+
var QWEN36_BASE = "https://huggingface.co/unsloth/Qwen3.6-35B-A3B-GGUF/resolve/main";
|
|
71196
|
+
var QWEN36_MMPROJ_URL = `${QWEN36_BASE}/mmproj-F16.gguf`;
|
|
71197
|
+
var QWEN36_URL = `${QWEN36_BASE}/Qwen3.6-35B-A3B-UD-Q4_K_M.gguf`;
|
|
70153
71198
|
var LLM_MODEL_CATALOG = [
|
|
70154
71199
|
QWEN,
|
|
70155
71200
|
LLAMA,
|
|
71201
|
+
SMOLVLM,
|
|
71202
|
+
QWEN3VL_2B,
|
|
70156
71203
|
{
|
|
70157
71204
|
meta: {
|
|
70158
|
-
id: "llm-
|
|
70159
|
-
label: "
|
|
70160
|
-
family: "
|
|
71205
|
+
id: "llm-qwen3.6-35b-a3b-ud-q4",
|
|
71206
|
+
label: "Qwen3.6 35B-A3B (vision)",
|
|
71207
|
+
family: "qwen3.6",
|
|
70161
71208
|
purpose: "vision",
|
|
70162
|
-
url:
|
|
70163
|
-
sha256: "
|
|
70164
|
-
sizeBytes:
|
|
70165
|
-
quantization: "Q4_K_M",
|
|
70166
|
-
minRamBytes:
|
|
70167
|
-
contextSizeDefault:
|
|
70168
|
-
mmprojUrl:
|
|
71209
|
+
url: QWEN36_URL,
|
|
71210
|
+
sha256: "ac0e2c1189e055faa36eff361580e79c5bd6f8e76bffb4ce547f167d53e31a61",
|
|
71211
|
+
sizeBytes: QWEN36_MODEL_BYTES,
|
|
71212
|
+
quantization: "UD-Q4_K_M",
|
|
71213
|
+
minRamBytes: 26 * GIB,
|
|
71214
|
+
contextSizeDefault: 32768,
|
|
71215
|
+
mmprojUrl: QWEN36_MMPROJ_URL
|
|
70169
71216
|
},
|
|
70170
71217
|
entry: {
|
|
70171
|
-
id: "llm-
|
|
70172
|
-
name: "
|
|
70173
|
-
description: "
|
|
71218
|
+
id: "llm-qwen3.6-35b-a3b-ud-q4",
|
|
71219
|
+
name: "Qwen3.6 35B-A3B (vision)",
|
|
71220
|
+
description: "qwen3.6 · UD-Q4_K_M · +mmproj",
|
|
70174
71221
|
formats: { gguf: {
|
|
70175
|
-
url:
|
|
70176
|
-
sizeMB: mb(
|
|
71222
|
+
url: QWEN36_URL,
|
|
71223
|
+
sizeMB: mb(QWEN36_MODEL_BYTES)
|
|
70177
71224
|
} },
|
|
70178
71225
|
inputSize: {
|
|
70179
71226
|
width: 0,
|
|
@@ -70181,9 +71228,9 @@ var LLM_MODEL_CATALOG = [
|
|
|
70181
71228
|
},
|
|
70182
71229
|
labels: [],
|
|
70183
71230
|
extraFiles: [{
|
|
70184
|
-
url:
|
|
70185
|
-
filename: "mmproj-
|
|
70186
|
-
sizeMB: mb(
|
|
71231
|
+
url: QWEN36_MMPROJ_URL,
|
|
71232
|
+
filename: "mmproj-F16.gguf",
|
|
71233
|
+
sizeMB: mb(QWEN36_MMPROJ_BYTES)
|
|
70187
71234
|
}]
|
|
70188
71235
|
}
|
|
70189
71236
|
}
|
|
@@ -70200,19 +71247,25 @@ function entryForRef(ref) {
|
|
|
70200
71247
|
}
|
|
70201
71248
|
if (ref.kind === "url") {
|
|
70202
71249
|
const id = `llm-custom-${createHash("sha1").update(ref.url).digest("hex").slice(0, 12)}`;
|
|
71250
|
+
const extraFiles = (ref.extraFiles ?? []).map((f) => ({
|
|
71251
|
+
url: f.url,
|
|
71252
|
+
filename: f.filename,
|
|
71253
|
+
sizeMB: mb(f.sizeBytes)
|
|
71254
|
+
}));
|
|
70203
71255
|
return { entry: {
|
|
70204
71256
|
id,
|
|
70205
|
-
name: id,
|
|
71257
|
+
name: ref.label ?? id,
|
|
70206
71258
|
description: "custom GGUF",
|
|
70207
71259
|
formats: { gguf: {
|
|
70208
71260
|
url: ref.url,
|
|
70209
|
-
sizeMB: 0
|
|
71261
|
+
sizeMB: ref.sizeBytes === void 0 ? 0 : mb(ref.sizeBytes)
|
|
70210
71262
|
} },
|
|
70211
71263
|
inputSize: {
|
|
70212
71264
|
width: 0,
|
|
70213
71265
|
height: 0
|
|
70214
71266
|
},
|
|
70215
|
-
labels: []
|
|
71267
|
+
labels: [],
|
|
71268
|
+
...extraFiles.length > 0 ? { extraFiles } : {}
|
|
70216
71269
|
} };
|
|
70217
71270
|
}
|
|
70218
71271
|
const id = `llm-path-${createHash("sha1").update(ref.path).digest("hex").slice(0, 12)}`;
|
|
@@ -70250,10 +71303,6 @@ function isNonEmptyFile(filePath) {
|
|
|
70250
71303
|
function siblingFilesFor(formatEntry) {
|
|
70251
71304
|
return formatEntry.isDirectory ? [] : formatEntry.files ?? [];
|
|
70252
71305
|
}
|
|
70253
|
-
/** Resolve a sibling's remote URL relative to the main file's directory. */
|
|
70254
|
-
function siblingUrl(mainUrl, sibling) {
|
|
70255
|
-
return mainUrl.replace(/[^/]+$/, sibling);
|
|
70256
|
-
}
|
|
70257
71306
|
/** Build fetch headers, including HF auth token for huggingface.co URLs */
|
|
70258
71307
|
function buildHeaders(url) {
|
|
70259
71308
|
const headers = { "User-Agent": "CamStack/1.0" };
|
|
@@ -70306,77 +71355,6 @@ async function downloadFile(url, destPath, onProgress) {
|
|
|
70306
71355
|
throw err;
|
|
70307
71356
|
}
|
|
70308
71357
|
}
|
|
70309
|
-
/**
|
|
70310
|
-
* Download every file in a HuggingFace directory bundle (e.g.,
|
|
70311
|
-
* `.mlpackage` / OpenVINO IR pair) atomically. `knownFiles` lists the
|
|
70312
|
-
* relative paths inside the directory; the function fetches each from
|
|
70313
|
-
* `${url}/${file}` and renames the staging directory only on full
|
|
70314
|
-
* success. Mirrors `ModelDownloadService.downloadDirectory` but
|
|
70315
|
-
* exposed as a standalone for catalog-less callers.
|
|
70316
|
-
*/
|
|
70317
|
-
async function downloadDirectory(url, destDir, knownFiles, onProgress) {
|
|
70318
|
-
const match = url.match(/huggingface\.co\/([^/]+\/[^/]+)\/resolve\/main\/(.+)/);
|
|
70319
|
-
if (!match) throw new Error(`Cannot parse HuggingFace URL: ${url}`);
|
|
70320
|
-
const [, repo, dirPath] = match;
|
|
70321
|
-
const files = (knownFiles ?? []).map((f) => ({
|
|
70322
|
-
relativePath: f,
|
|
70323
|
-
fileUrl: `https://huggingface.co/${repo}/resolve/main/${dirPath}/${f}`
|
|
70324
|
-
}));
|
|
70325
|
-
if (files.length === 0) throw new Error(`Directory bundle requires explicit \`files\` list (got none for ${url})`);
|
|
70326
|
-
const tmpDir = destDir + ".downloading";
|
|
70327
|
-
fs.rmSync(tmpDir, {
|
|
70328
|
-
recursive: true,
|
|
70329
|
-
force: true
|
|
70330
|
-
});
|
|
70331
|
-
fs.mkdirSync(tmpDir, { recursive: true });
|
|
70332
|
-
let totalDownloaded = 0;
|
|
70333
|
-
try {
|
|
70334
|
-
for (const file of files) {
|
|
70335
|
-
const destPath = path$1.join(tmpDir, file.relativePath);
|
|
70336
|
-
fs.mkdirSync(path$1.dirname(destPath), { recursive: true });
|
|
70337
|
-
await downloadFile(file.fileUrl, destPath, (downloaded, _total) => {
|
|
70338
|
-
onProgress?.(totalDownloaded + downloaded, void 0);
|
|
70339
|
-
});
|
|
70340
|
-
totalDownloaded += fs.statSync(destPath).size;
|
|
70341
|
-
}
|
|
70342
|
-
fs.rmSync(destDir, {
|
|
70343
|
-
recursive: true,
|
|
70344
|
-
force: true
|
|
70345
|
-
});
|
|
70346
|
-
fs.renameSync(tmpDir, destDir);
|
|
70347
|
-
} catch (err) {
|
|
70348
|
-
fs.rmSync(tmpDir, {
|
|
70349
|
-
recursive: true,
|
|
70350
|
-
force: true
|
|
70351
|
-
});
|
|
70352
|
-
throw err;
|
|
70353
|
-
}
|
|
70354
|
-
}
|
|
70355
|
-
/**
|
|
70356
|
-
* Resolve a `ModelCatalogEntry` against `modelsDir`: download model file
|
|
70357
|
-
* (or directory bundle) + extra files (labels JSON, charset dict, …),
|
|
70358
|
-
* skip if already on disk. Returns the local model path.
|
|
70359
|
-
*/
|
|
70360
|
-
async function ensureModel(modelsDir, entry, format, onProgress) {
|
|
70361
|
-
const formatEntry = entry.formats[format];
|
|
70362
|
-
if (!formatEntry) throw new Error(`Model "${entry.id}" has no ${format} format. Available: ${Object.keys(entry.formats).join(", ")}`);
|
|
70363
|
-
if (entry.extraFiles) for (const extra of entry.extraFiles) await downloadFile(extra.url, path$1.join(modelsDir, extra.filename));
|
|
70364
|
-
const filename = formatEntry.url.split("/").pop() ?? `${entry.id}.${format}`;
|
|
70365
|
-
const modelPath = path$1.join(modelsDir, filename);
|
|
70366
|
-
const siblings = siblingFilesFor(formatEntry);
|
|
70367
|
-
if (fs.existsSync(modelPath)) if (formatEntry.isDirectory && !fs.existsSync(path$1.join(modelPath, "Manifest.json"))) fs.rmSync(modelPath, {
|
|
70368
|
-
recursive: true,
|
|
70369
|
-
force: true
|
|
70370
|
-
});
|
|
70371
|
-
else if (siblings.some((f) => !isNonEmptyFile(path$1.join(modelsDir, f)))) {} else return modelPath;
|
|
70372
|
-
fs.mkdirSync(modelsDir, { recursive: true });
|
|
70373
|
-
if (formatEntry.isDirectory) await downloadDirectory(formatEntry.url, modelPath, formatEntry.files, onProgress);
|
|
70374
|
-
else {
|
|
70375
|
-
await downloadFile(formatEntry.url, modelPath, (downloaded, total) => onProgress?.(downloaded, total === 0 ? void 0 : total));
|
|
70376
|
-
for (const sibling of siblings) await downloadFile(siblingUrl(formatEntry.url, sibling), path$1.join(modelsDir, sibling));
|
|
70377
|
-
}
|
|
70378
|
-
return modelPath;
|
|
70379
|
-
}
|
|
70380
71358
|
/** Compute the on-disk path for a given model + format, even when not yet downloaded. */
|
|
70381
71359
|
function getModelFilePath(modelsDir, entry, format) {
|
|
70382
71360
|
const formatEntry = entry.formats[format];
|
|
@@ -70420,13 +71398,79 @@ promisify(gzip);
|
|
|
70420
71398
|
* Default `RuntimeModelOps` — the ONLY place the reused object-detection model
|
|
70421
71399
|
* mechanism is imported (the documented `@camstack/system/addon-utils`
|
|
70422
71400
|
* build-time-dep waiver that addon-post-analysis/addon-pipeline already use).
|
|
70423
|
-
* GGUFs ride `
|
|
70424
|
-
*
|
|
71401
|
+
* GGUFs ride the SHARED `downloadFile` (atomic `.downloading` + rename, HF
|
|
71402
|
+
* token headers, redirect following) — no bespoke fetcher (spec §4.2).
|
|
71403
|
+
*
|
|
71404
|
+
* ## Why this drives the file loop instead of calling `ensureModel`
|
|
71405
|
+
*
|
|
71406
|
+
* `ensureModel` downloads `extraFiles` FIRST and passes them NO progress
|
|
71407
|
+
* callback. That is invisible for a 40 kB labels JSON and unacceptable here: a
|
|
71408
|
+
* GGUF install is a 22 GB main file, up to N shards, and a 0.9 GB mmproj, and
|
|
71409
|
+
* under `ensureModel` every byte outside the main file moves in silence. A
|
|
71410
|
+
* multi-GB download that reports nothing reads as a hung node — the repo rule
|
|
71411
|
+
* is that a branch doing real work says so.
|
|
71412
|
+
*
|
|
71413
|
+
* So the loop is here, over the SAME `downloadFile`. What is gained: bytes
|
|
71414
|
+
* aggregated across the whole install, the name of the file currently moving,
|
|
71415
|
+
* and files already on disk excluded from the total rather than counted as
|
|
71416
|
+
* instantly-complete.
|
|
70425
71417
|
*/
|
|
70426
71418
|
var GGUF = "gguf";
|
|
71419
|
+
var BYTES_PER_MB = 1024 * 1024;
|
|
71420
|
+
/**
|
|
71421
|
+
* Main file first, then shards/mmproj. Deliberate: a gated or mistyped URL
|
|
71422
|
+
* fails on the file that matters before 0.9 GB of projector is spent on it.
|
|
71423
|
+
*/
|
|
71424
|
+
function planFiles(modelsDir, entry) {
|
|
71425
|
+
const out = [];
|
|
71426
|
+
const formatEntry = entry.formats[GGUF];
|
|
71427
|
+
if (formatEntry !== void 0) {
|
|
71428
|
+
const filename = formatEntry.url.split("/").pop() ?? `${entry.id}.${GGUF}`;
|
|
71429
|
+
out.push({
|
|
71430
|
+
url: formatEntry.url,
|
|
71431
|
+
destPath: path$1.join(modelsDir, filename),
|
|
71432
|
+
filename,
|
|
71433
|
+
expectedBytes: formatEntry.sizeMB * BYTES_PER_MB
|
|
71434
|
+
});
|
|
71435
|
+
}
|
|
71436
|
+
for (const extra of entry.extraFiles ?? []) out.push({
|
|
71437
|
+
url: extra.url,
|
|
71438
|
+
destPath: path$1.join(modelsDir, extra.filename),
|
|
71439
|
+
filename: extra.filename,
|
|
71440
|
+
expectedBytes: extra.sizeMB * BYTES_PER_MB
|
|
71441
|
+
});
|
|
71442
|
+
return out;
|
|
71443
|
+
}
|
|
70427
71444
|
function createDefaultModelOps(modelsDir) {
|
|
70428
71445
|
return {
|
|
70429
|
-
ensure: (entry, onProgress) =>
|
|
71446
|
+
ensure: async (entry, onProgress) => {
|
|
71447
|
+
if (entry.formats[GGUF] === void 0) throw new Error(`model ${entry.id} declares no gguf format`);
|
|
71448
|
+
const missing = planFiles(modelsDir, entry).filter((f) => !existsSync(f.destPath));
|
|
71449
|
+
const totalBytes = missing.reduce((sum, f) => sum + f.expectedBytes, 0);
|
|
71450
|
+
let carried = 0;
|
|
71451
|
+
for (const [index, file] of missing.entries()) {
|
|
71452
|
+
onProgress({
|
|
71453
|
+
file: file.filename,
|
|
71454
|
+
fileIndex: index + 1,
|
|
71455
|
+
fileCount: missing.length,
|
|
71456
|
+
downloadedBytes: carried,
|
|
71457
|
+
...totalBytes > 0 ? { totalBytes } : {}
|
|
71458
|
+
});
|
|
71459
|
+
await downloadFile(file.url, file.destPath, (downloaded) => {
|
|
71460
|
+
onProgress({
|
|
71461
|
+
file: file.filename,
|
|
71462
|
+
fileIndex: index + 1,
|
|
71463
|
+
fileCount: missing.length,
|
|
71464
|
+
downloadedBytes: carried + downloaded,
|
|
71465
|
+
...totalBytes > 0 ? { totalBytes } : {}
|
|
71466
|
+
});
|
|
71467
|
+
});
|
|
71468
|
+
carried += existsSync(file.destPath) ? statSync(file.destPath).size : file.expectedBytes;
|
|
71469
|
+
}
|
|
71470
|
+
const main = getModelFilePath(modelsDir, entry, GGUF);
|
|
71471
|
+
if (main === null) throw new Error(`no gguf path for model ${entry.id}`);
|
|
71472
|
+
return main;
|
|
71473
|
+
},
|
|
70430
71474
|
isDownloaded: (entry) => isModelDownloaded(modelsDir, entry, GGUF),
|
|
70431
71475
|
pathFor: (entry) => {
|
|
70432
71476
|
const p = getModelFilePath(modelsDir, entry, GGUF);
|
|
@@ -70440,6 +71484,387 @@ function createDefaultModelOps(modelsDir) {
|
|
|
70440
71484
|
};
|
|
70441
71485
|
}
|
|
70442
71486
|
//#endregion
|
|
71487
|
+
//#region src/runtime/crash-policy.ts
|
|
71488
|
+
var CrashPolicy = class {
|
|
71489
|
+
opts;
|
|
71490
|
+
crashes = /* @__PURE__ */ new Map();
|
|
71491
|
+
constructor(opts) {
|
|
71492
|
+
this.opts = opts;
|
|
71493
|
+
}
|
|
71494
|
+
recordCrash(id, now = Date.now()) {
|
|
71495
|
+
const cutoff = now - this.opts.windowMs;
|
|
71496
|
+
const recent = (this.crashes.get(id) ?? []).filter((t) => t >= cutoff);
|
|
71497
|
+
recent.push(now);
|
|
71498
|
+
this.crashes.set(id, recent);
|
|
71499
|
+
if (recent.length >= this.opts.maxCrashes) return {
|
|
71500
|
+
action: "failed",
|
|
71501
|
+
crashesInWindow: recent.length
|
|
71502
|
+
};
|
|
71503
|
+
const streak = recent.length;
|
|
71504
|
+
return {
|
|
71505
|
+
action: "respawn",
|
|
71506
|
+
backoffMs: Math.min(this.opts.maxBackoffMs, 500 * 2 ** Math.min(6, streak - 1)),
|
|
71507
|
+
crashesInWindow: streak
|
|
71508
|
+
};
|
|
71509
|
+
}
|
|
71510
|
+
crashesInWindow(id, now = Date.now()) {
|
|
71511
|
+
const cutoff = now - this.opts.windowMs;
|
|
71512
|
+
return (this.crashes.get(id) ?? []).filter((t) => t >= cutoff).length;
|
|
71513
|
+
}
|
|
71514
|
+
reset(id) {
|
|
71515
|
+
this.crashes.delete(id);
|
|
71516
|
+
}
|
|
71517
|
+
};
|
|
71518
|
+
var DEFAULT_CRASH_POLICY = {
|
|
71519
|
+
windowMs: 3e5,
|
|
71520
|
+
maxCrashes: 5,
|
|
71521
|
+
maxBackoffMs: 3e4
|
|
71522
|
+
};
|
|
71523
|
+
//#endregion
|
|
71524
|
+
//#region src/runtime/llama-supervisor.ts
|
|
71525
|
+
/**
|
|
71526
|
+
* `LlamaSupervisor` — single-child llama-server lifecycle on one node. Copies
|
|
71527
|
+
* the two proven in-repo patterns: the embedded-Python engine's spawn +
|
|
71528
|
+
* SIGTERM→deadline→SIGKILL escalation, and the CrashSupervisor breaker (bounded
|
|
71529
|
+
* respawn, never an infinite loop — D6). Guards the async ChildProcess 'error'
|
|
71530
|
+
* race the way fork-decode-worker's wrapChild does (7a95ab54): an uncaught late
|
|
71531
|
+
* error event must never become an uncaughtException.
|
|
71532
|
+
*
|
|
71533
|
+
* v1: at most one running child. Resource ceiling = llama-server flags +
|
|
71534
|
+
* idleStopMinutes ONLY (no RSS watchdog — operator decision #3).
|
|
71535
|
+
*/
|
|
71536
|
+
/**
|
|
71537
|
+
* Every llama-server flag a TYPED field above already owns, mapped to the
|
|
71538
|
+
* field that owns it.
|
|
71539
|
+
*
|
|
71540
|
+
* This map is the whole reconciliation between the typed tuning surface and
|
|
71541
|
+
* the free-text "additional arguments" box. Both exist because neither is
|
|
71542
|
+
* sufficient — the typed fields give the common knobs a validated control and
|
|
71543
|
+
* a default, and llama.cpp has a hundred flags nobody is going to model — but
|
|
71544
|
+
* a flag settable from BOTH is a bug generator: whichever one loses is a
|
|
71545
|
+
* control the operator watched do nothing. So the box is an escape hatch for
|
|
71546
|
+
* what is NOT modelled, and reaching into it for something that is gets
|
|
71547
|
+
* rejected by name.
|
|
71548
|
+
*/
|
|
71549
|
+
var OWNED_FLAGS = {
|
|
71550
|
+
"-m": "model",
|
|
71551
|
+
"--model": "model",
|
|
71552
|
+
"--host": "fixed to 127.0.0.1",
|
|
71553
|
+
"--port": "assigned by the supervisor",
|
|
71554
|
+
"-c": "contextSize",
|
|
71555
|
+
"--ctx-size": "contextSize",
|
|
71556
|
+
"-ngl": "gpuLayers",
|
|
71557
|
+
"--gpu-layers": "gpuLayers",
|
|
71558
|
+
"--n-gpu-layers": "gpuLayers",
|
|
71559
|
+
"-t": "threads",
|
|
71560
|
+
"--threads": "threads",
|
|
71561
|
+
"--parallel": "parallel",
|
|
71562
|
+
"-np": "parallel",
|
|
71563
|
+
"-b": "batchSize",
|
|
71564
|
+
"--batch-size": "batchSize",
|
|
71565
|
+
"-ub": "ubatchSize",
|
|
71566
|
+
"--ubatch-size": "ubatchSize",
|
|
71567
|
+
"-fa": "flashAttention",
|
|
71568
|
+
"--flash-attn": "flashAttention",
|
|
71569
|
+
"--mlock": "mlock",
|
|
71570
|
+
"--no-mmap": "noMmap",
|
|
71571
|
+
"-ctk": "cacheTypeK",
|
|
71572
|
+
"--cache-type-k": "cacheTypeK",
|
|
71573
|
+
"-ctv": "cacheTypeV",
|
|
71574
|
+
"--cache-type-v": "cacheTypeV",
|
|
71575
|
+
"--mmproj": "the vision model’s projector"
|
|
71576
|
+
};
|
|
71577
|
+
/**
|
|
71578
|
+
* Reject an `extraArgs` list that reaches for a flag a typed field owns.
|
|
71579
|
+
* `--flag=value` counts as `--flag`.
|
|
71580
|
+
*/
|
|
71581
|
+
function checkExtraArgs(extraArgs) {
|
|
71582
|
+
for (const token of extraArgs) {
|
|
71583
|
+
if (!token.startsWith("-")) continue;
|
|
71584
|
+
const flag = token.split("=")[0] ?? token;
|
|
71585
|
+
const owner = OWNED_FLAGS[flag];
|
|
71586
|
+
if (owner !== void 0) return {
|
|
71587
|
+
ok: false,
|
|
71588
|
+
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)`
|
|
71589
|
+
};
|
|
71590
|
+
}
|
|
71591
|
+
return { ok: true };
|
|
71592
|
+
}
|
|
71593
|
+
var HEALTH_GATE_INTERVAL_MS = 500;
|
|
71594
|
+
function defaultPickPort() {
|
|
71595
|
+
return new Promise((resolve, reject) => {
|
|
71596
|
+
const server = createServer();
|
|
71597
|
+
server.on("error", reject);
|
|
71598
|
+
server.listen(0, "127.0.0.1", () => {
|
|
71599
|
+
const addr = server.address();
|
|
71600
|
+
if (addr === null || typeof addr === "string") {
|
|
71601
|
+
server.close();
|
|
71602
|
+
reject(/* @__PURE__ */ new Error("failed to pick port"));
|
|
71603
|
+
return;
|
|
71604
|
+
}
|
|
71605
|
+
const { port } = addr;
|
|
71606
|
+
server.close(() => resolve(port));
|
|
71607
|
+
});
|
|
71608
|
+
});
|
|
71609
|
+
}
|
|
71610
|
+
/**
|
|
71611
|
+
* The argv, and it is the WHOLE tuning surface of a managed model.
|
|
71612
|
+
*
|
|
71613
|
+
* Every flag here is one the operator can set on the profile; nothing is
|
|
71614
|
+
* hard-coded that a real deployment needs to change. `--host 127.0.0.1` is the
|
|
71615
|
+
* one deliberate exception — a managed llama-server is reachable only from the
|
|
71616
|
+
* node that started it, and binding it wider would publish an unauthenticated
|
|
71617
|
+
* inference endpoint on the LAN.
|
|
71618
|
+
*/
|
|
71619
|
+
function buildLlamaArgs(cfg, port) {
|
|
71620
|
+
const args = [
|
|
71621
|
+
"-m",
|
|
71622
|
+
cfg.modelPath,
|
|
71623
|
+
"--host",
|
|
71624
|
+
"127.0.0.1",
|
|
71625
|
+
"--port",
|
|
71626
|
+
String(port),
|
|
71627
|
+
"-c",
|
|
71628
|
+
String(cfg.contextSize),
|
|
71629
|
+
"-ngl",
|
|
71630
|
+
String(cfg.gpuLayers),
|
|
71631
|
+
"--parallel",
|
|
71632
|
+
String(cfg.parallel)
|
|
71633
|
+
];
|
|
71634
|
+
if (cfg.threads !== void 0) args.push("-t", String(cfg.threads));
|
|
71635
|
+
if (cfg.batchSize !== void 0) args.push("-b", String(cfg.batchSize));
|
|
71636
|
+
if (cfg.ubatchSize !== void 0) args.push("-ub", String(cfg.ubatchSize));
|
|
71637
|
+
if (cfg.flashAttention === true) args.push("--flash-attn");
|
|
71638
|
+
if (cfg.mlock === true) args.push("--mlock");
|
|
71639
|
+
if (cfg.noMmap === true) args.push("--no-mmap");
|
|
71640
|
+
if (cfg.cacheTypeK !== void 0) args.push("--cache-type-k", cfg.cacheTypeK);
|
|
71641
|
+
if (cfg.cacheTypeV !== void 0) args.push("--cache-type-v", cfg.cacheTypeV);
|
|
71642
|
+
if (cfg.mmprojPath !== void 0) args.push("--mmproj", cfg.mmprojPath);
|
|
71643
|
+
args.push(...cfg.extraArgs ?? []);
|
|
71644
|
+
return args;
|
|
71645
|
+
}
|
|
71646
|
+
var LlamaSupervisor = class {
|
|
71647
|
+
deps;
|
|
71648
|
+
spawnFn;
|
|
71649
|
+
fetchFn;
|
|
71650
|
+
now;
|
|
71651
|
+
pickPort;
|
|
71652
|
+
healthPollMs;
|
|
71653
|
+
startTimeoutMs;
|
|
71654
|
+
killGraceMs;
|
|
71655
|
+
crashPolicy = new CrashPolicy(DEFAULT_CRASH_POLICY);
|
|
71656
|
+
supervisorId = "llama";
|
|
71657
|
+
child;
|
|
71658
|
+
state = "stopped";
|
|
71659
|
+
cfg;
|
|
71660
|
+
_port;
|
|
71661
|
+
lastError;
|
|
71662
|
+
lastActivity = 0;
|
|
71663
|
+
intentionalStop = false;
|
|
71664
|
+
healthGateTimer;
|
|
71665
|
+
startDeadlineTimer;
|
|
71666
|
+
healthPollTimer;
|
|
71667
|
+
idleTimer;
|
|
71668
|
+
constructor(deps) {
|
|
71669
|
+
this.deps = deps;
|
|
71670
|
+
this.spawnFn = deps.spawnFn ?? spawn;
|
|
71671
|
+
this.fetchFn = deps.fetchFn ?? fetch;
|
|
71672
|
+
this.now = deps.now ?? Date.now;
|
|
71673
|
+
this.pickPort = deps.pickPort ?? defaultPickPort;
|
|
71674
|
+
this.healthPollMs = deps.healthPollMs ?? 15e3;
|
|
71675
|
+
this.startTimeoutMs = deps.startTimeoutMs ?? 12e4;
|
|
71676
|
+
this.killGraceMs = deps.killGraceMs ?? 5e3;
|
|
71677
|
+
}
|
|
71678
|
+
get port() {
|
|
71679
|
+
return this._port;
|
|
71680
|
+
}
|
|
71681
|
+
status() {
|
|
71682
|
+
return {
|
|
71683
|
+
nodeId: this.cfg?.nodeId ?? "unknown",
|
|
71684
|
+
state: this.state,
|
|
71685
|
+
...this.child?.pid !== void 0 ? { pid: this.child.pid } : {},
|
|
71686
|
+
...this._port !== void 0 ? { port: this._port } : {},
|
|
71687
|
+
...this.cfg !== void 0 ? {
|
|
71688
|
+
modelPath: this.cfg.modelPath,
|
|
71689
|
+
modelId: this.cfg.modelId
|
|
71690
|
+
} : {},
|
|
71691
|
+
...this.lastError !== void 0 ? { lastError: this.lastError } : {},
|
|
71692
|
+
crashesInWindow: this.crashPolicy.crashesInWindow(this.supervisorId, this.now())
|
|
71693
|
+
};
|
|
71694
|
+
}
|
|
71695
|
+
noteActivity() {
|
|
71696
|
+
this.lastActivity = this.now();
|
|
71697
|
+
}
|
|
71698
|
+
sameConfig(cfg) {
|
|
71699
|
+
const c = this.cfg;
|
|
71700
|
+
if (c === void 0) return false;
|
|
71701
|
+
return c.modelPath === cfg.modelPath && c.mmprojPath === cfg.mmprojPath && c.contextSize === cfg.contextSize && c.gpuLayers === cfg.gpuLayers && c.threads === cfg.threads && c.parallel === cfg.parallel && c.batchSize === cfg.batchSize && c.ubatchSize === cfg.ubatchSize && c.flashAttention === cfg.flashAttention && c.mlock === cfg.mlock && c.noMmap === cfg.noMmap && c.cacheTypeK === cfg.cacheTypeK && c.cacheTypeV === cfg.cacheTypeV;
|
|
71702
|
+
}
|
|
71703
|
+
async start(cfg) {
|
|
71704
|
+
if (this.state === "ready" && this.sameConfig(cfg)) return this.status();
|
|
71705
|
+
if (this.child !== void 0) await this.stop();
|
|
71706
|
+
this.cfg = cfg;
|
|
71707
|
+
this.lastError = void 0;
|
|
71708
|
+
this.intentionalStop = false;
|
|
71709
|
+
this.crashPolicy.reset(this.supervisorId);
|
|
71710
|
+
await this.spawnChild(cfg);
|
|
71711
|
+
return this.status();
|
|
71712
|
+
}
|
|
71713
|
+
async spawnChild(cfg) {
|
|
71714
|
+
const port = await this.pickPort();
|
|
71715
|
+
this._port = port;
|
|
71716
|
+
this.state = "starting";
|
|
71717
|
+
const child = this.spawnFn(cfg.binaryPath, buildLlamaArgs(cfg, port), { stdio: "pipe" });
|
|
71718
|
+
this.child = child;
|
|
71719
|
+
child.on("error", (err) => {
|
|
71720
|
+
this.deps.logger.warn("llama-server error event", { meta: { error: String(err) } });
|
|
71721
|
+
});
|
|
71722
|
+
child.stdout?.on("data", (chunk) => this.logLines(chunk, false));
|
|
71723
|
+
child.stderr?.on("data", (chunk) => this.logLines(chunk, true));
|
|
71724
|
+
child.on("exit", (code, signal) => this.onExit(code, signal));
|
|
71725
|
+
this.beginHealthGate(port);
|
|
71726
|
+
}
|
|
71727
|
+
logLines(chunk, isErr) {
|
|
71728
|
+
for (const line of chunk.toString().split("\n")) {
|
|
71729
|
+
const trimmed = line.trim();
|
|
71730
|
+
if (trimmed.length === 0) continue;
|
|
71731
|
+
if (isErr) this.deps.logger.warn(trimmed);
|
|
71732
|
+
else this.deps.logger.debug(trimmed);
|
|
71733
|
+
}
|
|
71734
|
+
}
|
|
71735
|
+
beginHealthGate(port) {
|
|
71736
|
+
const deadlineAt = this.now() + this.startTimeoutMs;
|
|
71737
|
+
const poll = async () => {
|
|
71738
|
+
if (this.state !== "starting") return;
|
|
71739
|
+
if (this.now() >= deadlineAt) {
|
|
71740
|
+
this.lastError = `health gate timed out after ${this.startTimeoutMs}ms`;
|
|
71741
|
+
this.deps.logger.warn(this.lastError);
|
|
71742
|
+
this.intentionalStop = true;
|
|
71743
|
+
this.killChild("SIGKILL");
|
|
71744
|
+
this.state = "crashed";
|
|
71745
|
+
return;
|
|
71746
|
+
}
|
|
71747
|
+
const healthy = await this.probeHealth(port);
|
|
71748
|
+
if (this.state !== "starting") return;
|
|
71749
|
+
if (healthy) {
|
|
71750
|
+
this.onReady();
|
|
71751
|
+
return;
|
|
71752
|
+
}
|
|
71753
|
+
this.healthGateTimer = setTimeout(() => void poll(), HEALTH_GATE_INTERVAL_MS);
|
|
71754
|
+
this.healthGateTimer.unref?.();
|
|
71755
|
+
};
|
|
71756
|
+
poll();
|
|
71757
|
+
}
|
|
71758
|
+
async probeHealth(port) {
|
|
71759
|
+
try {
|
|
71760
|
+
return (await this.fetchFn(`http://127.0.0.1:${port}/health`)).status === 200;
|
|
71761
|
+
} catch {
|
|
71762
|
+
return false;
|
|
71763
|
+
}
|
|
71764
|
+
}
|
|
71765
|
+
onReady() {
|
|
71766
|
+
this.clearStartTimers();
|
|
71767
|
+
this.state = "ready";
|
|
71768
|
+
this.lastError = void 0;
|
|
71769
|
+
this.noteActivity();
|
|
71770
|
+
this.startHealthPoll();
|
|
71771
|
+
this.startIdleWatch();
|
|
71772
|
+
}
|
|
71773
|
+
startHealthPoll() {
|
|
71774
|
+
this.healthPollTimer = setInterval(() => {
|
|
71775
|
+
if (this.state !== "ready" || this._port === void 0) return;
|
|
71776
|
+
this.probeHealth(this._port).then((healthy) => {
|
|
71777
|
+
if (!healthy && this.state === "ready") {
|
|
71778
|
+
this.deps.logger.warn("llama-server health poll failed → treating as crash");
|
|
71779
|
+
this.killChild("SIGKILL");
|
|
71780
|
+
}
|
|
71781
|
+
});
|
|
71782
|
+
}, this.healthPollMs);
|
|
71783
|
+
this.healthPollTimer.unref?.();
|
|
71784
|
+
}
|
|
71785
|
+
startIdleWatch() {
|
|
71786
|
+
const cfg = this.cfg;
|
|
71787
|
+
if (cfg === void 0 || cfg.idleStopMinutes <= 0) return;
|
|
71788
|
+
const idleMs = cfg.idleStopMinutes * 6e4;
|
|
71789
|
+
this.idleTimer = setInterval(() => {
|
|
71790
|
+
if (this.state !== "ready") return;
|
|
71791
|
+
if (this.now() - this.lastActivity >= idleMs) {
|
|
71792
|
+
this.deps.logger.info("llama-server idle-stop");
|
|
71793
|
+
this.stop();
|
|
71794
|
+
}
|
|
71795
|
+
}, Math.min(idleMs, 3e4));
|
|
71796
|
+
this.idleTimer.unref?.();
|
|
71797
|
+
}
|
|
71798
|
+
onExit(code, signal) {
|
|
71799
|
+
this.clearAllTimers();
|
|
71800
|
+
this.child = void 0;
|
|
71801
|
+
if (this.intentionalStop) {
|
|
71802
|
+
this.state = "stopped";
|
|
71803
|
+
return;
|
|
71804
|
+
}
|
|
71805
|
+
this.deps.logger.warn("llama-server exited unexpectedly", { meta: {
|
|
71806
|
+
code,
|
|
71807
|
+
signal
|
|
71808
|
+
} });
|
|
71809
|
+
const decision = this.crashPolicy.recordCrash(this.supervisorId, this.now());
|
|
71810
|
+
if (decision.action === "failed") {
|
|
71811
|
+
this.state = "failed";
|
|
71812
|
+
this.lastError = `crash breaker tripped (${decision.crashesInWindow} crashes)`;
|
|
71813
|
+
return;
|
|
71814
|
+
}
|
|
71815
|
+
this.state = "crashed";
|
|
71816
|
+
const cfg = this.cfg;
|
|
71817
|
+
if (cfg === void 0) return;
|
|
71818
|
+
setTimeout(() => {
|
|
71819
|
+
if (this.state !== "crashed") return;
|
|
71820
|
+
this.spawnChild(cfg).catch((err) => {
|
|
71821
|
+
this.lastError = err instanceof Error ? err.message : String(err);
|
|
71822
|
+
this.state = "crashed";
|
|
71823
|
+
});
|
|
71824
|
+
}, decision.backoffMs).unref?.();
|
|
71825
|
+
}
|
|
71826
|
+
killChild(signal) {
|
|
71827
|
+
this.child?.kill(signal);
|
|
71828
|
+
}
|
|
71829
|
+
async stop() {
|
|
71830
|
+
this.intentionalStop = true;
|
|
71831
|
+
this.clearAllTimers();
|
|
71832
|
+
this.crashPolicy.reset(this.supervisorId);
|
|
71833
|
+
const child = this.child;
|
|
71834
|
+
if (child === void 0) {
|
|
71835
|
+
this.state = "stopped";
|
|
71836
|
+
return;
|
|
71837
|
+
}
|
|
71838
|
+
child.kill("SIGTERM");
|
|
71839
|
+
await new Promise((resolve) => {
|
|
71840
|
+
const grace = setTimeout(() => {
|
|
71841
|
+
child.kill("SIGKILL");
|
|
71842
|
+
resolve();
|
|
71843
|
+
}, this.killGraceMs);
|
|
71844
|
+
grace.unref?.();
|
|
71845
|
+
child.once("exit", () => {
|
|
71846
|
+
clearTimeout(grace);
|
|
71847
|
+
resolve();
|
|
71848
|
+
});
|
|
71849
|
+
});
|
|
71850
|
+
this.child = void 0;
|
|
71851
|
+
this.state = "stopped";
|
|
71852
|
+
}
|
|
71853
|
+
clearStartTimers() {
|
|
71854
|
+
if (this.healthGateTimer !== void 0) clearTimeout(this.healthGateTimer);
|
|
71855
|
+
if (this.startDeadlineTimer !== void 0) clearTimeout(this.startDeadlineTimer);
|
|
71856
|
+
this.healthGateTimer = void 0;
|
|
71857
|
+
this.startDeadlineTimer = void 0;
|
|
71858
|
+
}
|
|
71859
|
+
clearAllTimers() {
|
|
71860
|
+
this.clearStartTimers();
|
|
71861
|
+
if (this.healthPollTimer !== void 0) clearInterval(this.healthPollTimer);
|
|
71862
|
+
if (this.idleTimer !== void 0) clearInterval(this.idleTimer);
|
|
71863
|
+
this.healthPollTimer = void 0;
|
|
71864
|
+
this.idleTimer = void 0;
|
|
71865
|
+
}
|
|
71866
|
+
};
|
|
71867
|
+
//#endregion
|
|
70443
71868
|
//#region src/runtime/sha256.ts
|
|
70444
71869
|
/**
|
|
70445
71870
|
* File sha256 — a local copy of the private `computeSha256` at
|
|
@@ -70473,11 +71898,23 @@ function basename(url) {
|
|
|
70473
71898
|
function catalogIdForFile(file) {
|
|
70474
71899
|
return LLM_MODEL_CATALOG.find((m) => basename(m.meta.url) === file)?.meta.id;
|
|
70475
71900
|
}
|
|
71901
|
+
/**
|
|
71902
|
+
* The projector among the extra files — matched by NAME, not by position.
|
|
71903
|
+
*
|
|
71904
|
+
* `extraFiles[0]` was safe while the only extra a GGUF entry ever had was an
|
|
71905
|
+
* mmproj. A split GGUF puts shards 2..N in the same list, so index 0 is now
|
|
71906
|
+
* routinely a weights shard, and passing one to `--mmproj` starts llama-server
|
|
71907
|
+
* against a file that is not a projector.
|
|
71908
|
+
*/
|
|
70476
71909
|
function mmprojFilename(entry) {
|
|
70477
|
-
return entry.extraFiles?.
|
|
71910
|
+
return entry.extraFiles?.find((f) => f.filename.toLowerCase().startsWith("mmproj"))?.filename;
|
|
71911
|
+
}
|
|
71912
|
+
function gb(bytes) {
|
|
71913
|
+
return `${(bytes / 1e9).toFixed(2)} GB`;
|
|
70478
71914
|
}
|
|
70479
71915
|
function createLlmRuntimeProvider(deps) {
|
|
70480
71916
|
let downloadProgress;
|
|
71917
|
+
let download;
|
|
70481
71918
|
async function resolvePaths(runtime) {
|
|
70482
71919
|
const resolution = entryForRef(runtime.model);
|
|
70483
71920
|
if (resolution === null) throw new Error("unknown model reference");
|
|
@@ -70504,6 +71941,8 @@ function createLlmRuntimeProvider(deps) {
|
|
|
70504
71941
|
return { ok: true };
|
|
70505
71942
|
}
|
|
70506
71943
|
async function ensureStartedInternal(runtime) {
|
|
71944
|
+
const argCheck = checkExtraArgs(runtime.extraArgs);
|
|
71945
|
+
if (!argCheck.ok) throw new Error(argCheck.message);
|
|
70507
71946
|
const binaryPath = await deps.ensureBinary();
|
|
70508
71947
|
const paths = await resolvePaths(runtime);
|
|
70509
71948
|
const startCfg = {
|
|
@@ -70522,16 +71961,78 @@ function createLlmRuntimeProvider(deps) {
|
|
|
70522
71961
|
noMmap: runtime.noMmap,
|
|
70523
71962
|
...runtime.cacheTypeK !== void 0 ? { cacheTypeK: runtime.cacheTypeK } : {},
|
|
70524
71963
|
...runtime.cacheTypeV !== void 0 ? { cacheTypeV: runtime.cacheTypeV } : {},
|
|
71964
|
+
extraArgs: runtime.extraArgs,
|
|
70525
71965
|
idleStopMinutes: runtime.idleStopMinutes,
|
|
70526
71966
|
binaryPath
|
|
70527
71967
|
};
|
|
70528
71968
|
return deps.supervisor.start(startCfg);
|
|
70529
71969
|
}
|
|
71970
|
+
/**
|
|
71971
|
+
* sha256 every artifact whose digest the reference pinned — the main file
|
|
71972
|
+
* AND the extras.
|
|
71973
|
+
*
|
|
71974
|
+
* Verifying only the main file was the gap: a truncated or swapped mmproj is
|
|
71975
|
+
* exactly as fatal to llama-server as a bad weights file, and a resolved HF
|
|
71976
|
+
* reference carries a digest for every artifact (LFS `oid`) so there is no
|
|
71977
|
+
* reason to check one and trust the rest.
|
|
71978
|
+
*
|
|
71979
|
+
* This pass reads tens of GB and takes minutes; it is a REPORTED phase, not
|
|
71980
|
+
* a silent tail, because a progress bar frozen at 100% is the shape of a
|
|
71981
|
+
* hang.
|
|
71982
|
+
*/
|
|
71983
|
+
async function verifyDigests(entry, model, startedAt) {
|
|
71984
|
+
if (model.kind !== "url") return;
|
|
71985
|
+
const targets = [];
|
|
71986
|
+
if (model.sha256 !== void 0) targets.push({
|
|
71987
|
+
filePath: deps.modelOps.pathFor(entry),
|
|
71988
|
+
sha256: model.sha256,
|
|
71989
|
+
name: basename(model.url)
|
|
71990
|
+
});
|
|
71991
|
+
for (const extra of model.extraFiles ?? []) {
|
|
71992
|
+
if (extra.sha256 === void 0) continue;
|
|
71993
|
+
targets.push({
|
|
71994
|
+
filePath: deps.modelOps.extraFilePath(entry, extra.filename),
|
|
71995
|
+
sha256: extra.sha256,
|
|
71996
|
+
name: extra.filename
|
|
71997
|
+
});
|
|
71998
|
+
}
|
|
71999
|
+
if (targets.length === 0) return;
|
|
72000
|
+
const sha256 = deps.fileSha256 ?? fileSha256;
|
|
72001
|
+
for (const [index, target] of targets.entries()) {
|
|
72002
|
+
download = {
|
|
72003
|
+
phase: "verifying",
|
|
72004
|
+
file: target.name,
|
|
72005
|
+
fileIndex: index + 1,
|
|
72006
|
+
fileCount: targets.length,
|
|
72007
|
+
downloadedBytes: 0
|
|
72008
|
+
};
|
|
72009
|
+
deps.logger.info("llm model verifying digest", { meta: {
|
|
72010
|
+
nodeId: deps.nodeId,
|
|
72011
|
+
modelId: entry.id,
|
|
72012
|
+
file: target.name
|
|
72013
|
+
} });
|
|
72014
|
+
const digest = await sha256(target.filePath);
|
|
72015
|
+
if (digest !== target.sha256) {
|
|
72016
|
+
deps.logger.error("llm model digest mismatch; discarding the download", { meta: {
|
|
72017
|
+
nodeId: deps.nodeId,
|
|
72018
|
+
modelId: entry.id,
|
|
72019
|
+
file: target.name,
|
|
72020
|
+
expected: target.sha256,
|
|
72021
|
+
actual: digest,
|
|
72022
|
+
elapsedMs: Date.now() - startedAt
|
|
72023
|
+
} });
|
|
72024
|
+
await deps.modelOps.delete(entry);
|
|
72025
|
+
await fsp.rm(target.filePath, { force: true });
|
|
72026
|
+
throw new Error(`sha256 mismatch for ${target.name}: expected ${target.sha256}, got ${digest}`);
|
|
72027
|
+
}
|
|
72028
|
+
}
|
|
72029
|
+
}
|
|
70530
72030
|
function status() {
|
|
70531
72031
|
return {
|
|
70532
72032
|
...deps.supervisor.status(),
|
|
70533
72033
|
nodeId: deps.nodeId,
|
|
70534
|
-
...downloadProgress !== void 0 ? { downloadProgress } : {}
|
|
72034
|
+
...downloadProgress !== void 0 ? { downloadProgress } : {},
|
|
72035
|
+
...download !== void 0 ? { download } : {}
|
|
70535
72036
|
};
|
|
70536
72037
|
}
|
|
70537
72038
|
return {
|
|
@@ -70599,24 +72100,91 @@ function createLlmRuntimeProvider(deps) {
|
|
|
70599
72100
|
await deps.supervisor.stop();
|
|
70600
72101
|
},
|
|
70601
72102
|
status: async () => status(),
|
|
72103
|
+
/**
|
|
72104
|
+
* Install a model on THIS node.
|
|
72105
|
+
*
|
|
72106
|
+
* Loud on purpose. This is the longest-running operation the addon has —
|
|
72107
|
+
* tens of minutes for a 23 GB vision model — and until now it emitted not
|
|
72108
|
+
* one log line, so an install that stalled on a gated URL or a full disk
|
|
72109
|
+
* was indistinguishable from one that was simply slow. Every phase
|
|
72110
|
+
* transition is a line, and every line carries the node.
|
|
72111
|
+
*/
|
|
70602
72112
|
installModel: async ({ model }) => {
|
|
70603
72113
|
const resolution = entryForRef(model);
|
|
70604
72114
|
if (resolution === null) throw new Error("unknown model reference");
|
|
70605
|
-
if (resolution.localPathOverride !== void 0)
|
|
72115
|
+
if (resolution.localPathOverride !== void 0) {
|
|
72116
|
+
deps.logger.info("llm model is pre-provisioned; nothing to download", { meta: {
|
|
72117
|
+
nodeId: deps.nodeId,
|
|
72118
|
+
path: resolution.localPathOverride
|
|
72119
|
+
} });
|
|
72120
|
+
return;
|
|
72121
|
+
}
|
|
72122
|
+
const { entry } = resolution;
|
|
72123
|
+
const declaredBytes = model.kind === "url" ? model.sizeBytes : void 0;
|
|
72124
|
+
const startedAt = Date.now();
|
|
72125
|
+
deps.logger.info("llm model install started", { meta: {
|
|
72126
|
+
nodeId: deps.nodeId,
|
|
72127
|
+
modelId: entry.id,
|
|
72128
|
+
url: entry.formats.gguf?.url,
|
|
72129
|
+
extraFiles: (entry.extraFiles ?? []).map((f) => f.filename),
|
|
72130
|
+
...declaredBytes !== void 0 ? {
|
|
72131
|
+
declaredBytes,
|
|
72132
|
+
declaredSize: gb(declaredBytes)
|
|
72133
|
+
} : {}
|
|
72134
|
+
} });
|
|
70606
72135
|
downloadProgress = 0;
|
|
72136
|
+
download = {
|
|
72137
|
+
phase: "downloading",
|
|
72138
|
+
file: "",
|
|
72139
|
+
fileIndex: 0,
|
|
72140
|
+
fileCount: 0,
|
|
72141
|
+
downloadedBytes: 0
|
|
72142
|
+
};
|
|
72143
|
+
let lastLoggedDecile = -1;
|
|
70607
72144
|
try {
|
|
70608
|
-
await deps.modelOps.ensure(
|
|
70609
|
-
|
|
70610
|
-
|
|
70611
|
-
|
|
70612
|
-
|
|
70613
|
-
|
|
70614
|
-
|
|
70615
|
-
|
|
72145
|
+
await deps.modelOps.ensure(entry, (progress) => {
|
|
72146
|
+
const fraction = progress.totalBytes !== void 0 && progress.totalBytes > 0 ? Math.min(1, progress.downloadedBytes / progress.totalBytes) : void 0;
|
|
72147
|
+
downloadProgress = fraction;
|
|
72148
|
+
download = {
|
|
72149
|
+
phase: "downloading",
|
|
72150
|
+
file: progress.file,
|
|
72151
|
+
fileIndex: progress.fileIndex,
|
|
72152
|
+
fileCount: progress.fileCount,
|
|
72153
|
+
downloadedBytes: progress.downloadedBytes,
|
|
72154
|
+
...progress.totalBytes !== void 0 ? { totalBytes: progress.totalBytes } : {}
|
|
72155
|
+
};
|
|
72156
|
+
const decile = fraction === void 0 ? -1 : Math.floor(fraction * 10);
|
|
72157
|
+
if (decile > lastLoggedDecile) {
|
|
72158
|
+
lastLoggedDecile = decile;
|
|
72159
|
+
deps.logger.info("llm model download progress", { meta: {
|
|
72160
|
+
nodeId: deps.nodeId,
|
|
72161
|
+
modelId: entry.id,
|
|
72162
|
+
file: progress.file,
|
|
72163
|
+
fileIndex: progress.fileIndex,
|
|
72164
|
+
fileCount: progress.fileCount,
|
|
72165
|
+
downloadedBytes: progress.downloadedBytes,
|
|
72166
|
+
downloaded: gb(progress.downloadedBytes),
|
|
72167
|
+
...progress.totalBytes !== void 0 ? { total: gb(progress.totalBytes) } : {}
|
|
72168
|
+
} });
|
|
70616
72169
|
}
|
|
70617
|
-
}
|
|
72170
|
+
});
|
|
72171
|
+
await verifyDigests(entry, model, startedAt);
|
|
72172
|
+
deps.logger.info("llm model install complete", { meta: {
|
|
72173
|
+
nodeId: deps.nodeId,
|
|
72174
|
+
modelId: entry.id,
|
|
72175
|
+
elapsedMs: Date.now() - startedAt
|
|
72176
|
+
} });
|
|
72177
|
+
} catch (err) {
|
|
72178
|
+
deps.logger.error("llm model install failed", { meta: {
|
|
72179
|
+
nodeId: deps.nodeId,
|
|
72180
|
+
modelId: entry.id,
|
|
72181
|
+
elapsedMs: Date.now() - startedAt,
|
|
72182
|
+
error: err instanceof Error ? err.message : String(err)
|
|
72183
|
+
} });
|
|
72184
|
+
throw err;
|
|
70618
72185
|
} finally {
|
|
70619
72186
|
downloadProgress = void 0;
|
|
72187
|
+
download = void 0;
|
|
70620
72188
|
}
|
|
70621
72189
|
},
|
|
70622
72190
|
deleteModel: async ({ file }) => {
|
|
@@ -70630,6 +72198,7 @@ function createLlmRuntimeProvider(deps) {
|
|
|
70630
72198
|
return {
|
|
70631
72199
|
file: f.file,
|
|
70632
72200
|
sizeBytes: f.sizeBytes,
|
|
72201
|
+
path: path$1.join(deps.modelsDir, f.file),
|
|
70633
72202
|
...catalogId !== void 0 ? { catalogId } : {}
|
|
70634
72203
|
};
|
|
70635
72204
|
});
|
|
@@ -70736,6 +72305,7 @@ async function assembleAi(deps) {
|
|
|
70736
72305
|
...deps.runtimeApi !== void 0 ? { runtime: createRuntimeClient(deps.runtimeApi) } : {},
|
|
70737
72306
|
...deps.distributeModel !== void 0 ? { distributeModel: deps.distributeModel } : {},
|
|
70738
72307
|
catalog: LLM_MODEL_CATALOG.map((m) => m.meta),
|
|
72308
|
+
hfToken: () => process.env["HF_TOKEN"] ?? process.env["HUGGING_FACE_HUB_TOKEN"],
|
|
70739
72309
|
logger: deps.logger.child("llm")
|
|
70740
72310
|
});
|
|
70741
72311
|
registrations.push({
|
|
@@ -70752,329 +72322,6 @@ async function assembleAi(deps) {
|
|
|
70752
72322
|
};
|
|
70753
72323
|
}
|
|
70754
72324
|
//#endregion
|
|
70755
|
-
//#region src/runtime/crash-policy.ts
|
|
70756
|
-
var CrashPolicy = class {
|
|
70757
|
-
opts;
|
|
70758
|
-
crashes = /* @__PURE__ */ new Map();
|
|
70759
|
-
constructor(opts) {
|
|
70760
|
-
this.opts = opts;
|
|
70761
|
-
}
|
|
70762
|
-
recordCrash(id, now = Date.now()) {
|
|
70763
|
-
const cutoff = now - this.opts.windowMs;
|
|
70764
|
-
const recent = (this.crashes.get(id) ?? []).filter((t) => t >= cutoff);
|
|
70765
|
-
recent.push(now);
|
|
70766
|
-
this.crashes.set(id, recent);
|
|
70767
|
-
if (recent.length >= this.opts.maxCrashes) return {
|
|
70768
|
-
action: "failed",
|
|
70769
|
-
crashesInWindow: recent.length
|
|
70770
|
-
};
|
|
70771
|
-
const streak = recent.length;
|
|
70772
|
-
return {
|
|
70773
|
-
action: "respawn",
|
|
70774
|
-
backoffMs: Math.min(this.opts.maxBackoffMs, 500 * 2 ** Math.min(6, streak - 1)),
|
|
70775
|
-
crashesInWindow: streak
|
|
70776
|
-
};
|
|
70777
|
-
}
|
|
70778
|
-
crashesInWindow(id, now = Date.now()) {
|
|
70779
|
-
const cutoff = now - this.opts.windowMs;
|
|
70780
|
-
return (this.crashes.get(id) ?? []).filter((t) => t >= cutoff).length;
|
|
70781
|
-
}
|
|
70782
|
-
reset(id) {
|
|
70783
|
-
this.crashes.delete(id);
|
|
70784
|
-
}
|
|
70785
|
-
};
|
|
70786
|
-
var DEFAULT_CRASH_POLICY = {
|
|
70787
|
-
windowMs: 3e5,
|
|
70788
|
-
maxCrashes: 5,
|
|
70789
|
-
maxBackoffMs: 3e4
|
|
70790
|
-
};
|
|
70791
|
-
//#endregion
|
|
70792
|
-
//#region src/runtime/llama-supervisor.ts
|
|
70793
|
-
/**
|
|
70794
|
-
* `LlamaSupervisor` — single-child llama-server lifecycle on one node. Copies
|
|
70795
|
-
* the two proven in-repo patterns: the embedded-Python engine's spawn +
|
|
70796
|
-
* SIGTERM→deadline→SIGKILL escalation, and the CrashSupervisor breaker (bounded
|
|
70797
|
-
* respawn, never an infinite loop — D6). Guards the async ChildProcess 'error'
|
|
70798
|
-
* race the way fork-decode-worker's wrapChild does (7a95ab54): an uncaught late
|
|
70799
|
-
* error event must never become an uncaughtException.
|
|
70800
|
-
*
|
|
70801
|
-
* v1: at most one running child. Resource ceiling = llama-server flags +
|
|
70802
|
-
* idleStopMinutes ONLY (no RSS watchdog — operator decision #3).
|
|
70803
|
-
*/
|
|
70804
|
-
var HEALTH_GATE_INTERVAL_MS = 500;
|
|
70805
|
-
function defaultPickPort() {
|
|
70806
|
-
return new Promise((resolve, reject) => {
|
|
70807
|
-
const server = createServer();
|
|
70808
|
-
server.on("error", reject);
|
|
70809
|
-
server.listen(0, "127.0.0.1", () => {
|
|
70810
|
-
const addr = server.address();
|
|
70811
|
-
if (addr === null || typeof addr === "string") {
|
|
70812
|
-
server.close();
|
|
70813
|
-
reject(/* @__PURE__ */ new Error("failed to pick port"));
|
|
70814
|
-
return;
|
|
70815
|
-
}
|
|
70816
|
-
const { port } = addr;
|
|
70817
|
-
server.close(() => resolve(port));
|
|
70818
|
-
});
|
|
70819
|
-
});
|
|
70820
|
-
}
|
|
70821
|
-
/**
|
|
70822
|
-
* The argv, and it is the WHOLE tuning surface of a managed model.
|
|
70823
|
-
*
|
|
70824
|
-
* Every flag here is one the operator can set on the profile; nothing is
|
|
70825
|
-
* hard-coded that a real deployment needs to change. `--host 127.0.0.1` is the
|
|
70826
|
-
* one deliberate exception — a managed llama-server is reachable only from the
|
|
70827
|
-
* node that started it, and binding it wider would publish an unauthenticated
|
|
70828
|
-
* inference endpoint on the LAN.
|
|
70829
|
-
*/
|
|
70830
|
-
function buildLlamaArgs(cfg, port) {
|
|
70831
|
-
const args = [
|
|
70832
|
-
"-m",
|
|
70833
|
-
cfg.modelPath,
|
|
70834
|
-
"--host",
|
|
70835
|
-
"127.0.0.1",
|
|
70836
|
-
"--port",
|
|
70837
|
-
String(port),
|
|
70838
|
-
"-c",
|
|
70839
|
-
String(cfg.contextSize),
|
|
70840
|
-
"-ngl",
|
|
70841
|
-
String(cfg.gpuLayers),
|
|
70842
|
-
"--parallel",
|
|
70843
|
-
String(cfg.parallel)
|
|
70844
|
-
];
|
|
70845
|
-
if (cfg.threads !== void 0) args.push("-t", String(cfg.threads));
|
|
70846
|
-
if (cfg.batchSize !== void 0) args.push("-b", String(cfg.batchSize));
|
|
70847
|
-
if (cfg.ubatchSize !== void 0) args.push("-ub", String(cfg.ubatchSize));
|
|
70848
|
-
if (cfg.flashAttention === true) args.push("--flash-attn");
|
|
70849
|
-
if (cfg.mlock === true) args.push("--mlock");
|
|
70850
|
-
if (cfg.noMmap === true) args.push("--no-mmap");
|
|
70851
|
-
if (cfg.cacheTypeK !== void 0) args.push("--cache-type-k", cfg.cacheTypeK);
|
|
70852
|
-
if (cfg.cacheTypeV !== void 0) args.push("--cache-type-v", cfg.cacheTypeV);
|
|
70853
|
-
if (cfg.mmprojPath !== void 0) args.push("--mmproj", cfg.mmprojPath);
|
|
70854
|
-
return args;
|
|
70855
|
-
}
|
|
70856
|
-
var LlamaSupervisor = class {
|
|
70857
|
-
deps;
|
|
70858
|
-
spawnFn;
|
|
70859
|
-
fetchFn;
|
|
70860
|
-
now;
|
|
70861
|
-
pickPort;
|
|
70862
|
-
healthPollMs;
|
|
70863
|
-
startTimeoutMs;
|
|
70864
|
-
killGraceMs;
|
|
70865
|
-
crashPolicy = new CrashPolicy(DEFAULT_CRASH_POLICY);
|
|
70866
|
-
supervisorId = "llama";
|
|
70867
|
-
child;
|
|
70868
|
-
state = "stopped";
|
|
70869
|
-
cfg;
|
|
70870
|
-
_port;
|
|
70871
|
-
lastError;
|
|
70872
|
-
lastActivity = 0;
|
|
70873
|
-
intentionalStop = false;
|
|
70874
|
-
healthGateTimer;
|
|
70875
|
-
startDeadlineTimer;
|
|
70876
|
-
healthPollTimer;
|
|
70877
|
-
idleTimer;
|
|
70878
|
-
constructor(deps) {
|
|
70879
|
-
this.deps = deps;
|
|
70880
|
-
this.spawnFn = deps.spawnFn ?? spawn;
|
|
70881
|
-
this.fetchFn = deps.fetchFn ?? fetch;
|
|
70882
|
-
this.now = deps.now ?? Date.now;
|
|
70883
|
-
this.pickPort = deps.pickPort ?? defaultPickPort;
|
|
70884
|
-
this.healthPollMs = deps.healthPollMs ?? 15e3;
|
|
70885
|
-
this.startTimeoutMs = deps.startTimeoutMs ?? 12e4;
|
|
70886
|
-
this.killGraceMs = deps.killGraceMs ?? 5e3;
|
|
70887
|
-
}
|
|
70888
|
-
get port() {
|
|
70889
|
-
return this._port;
|
|
70890
|
-
}
|
|
70891
|
-
status() {
|
|
70892
|
-
return {
|
|
70893
|
-
nodeId: this.cfg?.nodeId ?? "unknown",
|
|
70894
|
-
state: this.state,
|
|
70895
|
-
...this.child?.pid !== void 0 ? { pid: this.child.pid } : {},
|
|
70896
|
-
...this._port !== void 0 ? { port: this._port } : {},
|
|
70897
|
-
...this.cfg !== void 0 ? {
|
|
70898
|
-
modelPath: this.cfg.modelPath,
|
|
70899
|
-
modelId: this.cfg.modelId
|
|
70900
|
-
} : {},
|
|
70901
|
-
...this.lastError !== void 0 ? { lastError: this.lastError } : {},
|
|
70902
|
-
crashesInWindow: this.crashPolicy.crashesInWindow(this.supervisorId, this.now())
|
|
70903
|
-
};
|
|
70904
|
-
}
|
|
70905
|
-
noteActivity() {
|
|
70906
|
-
this.lastActivity = this.now();
|
|
70907
|
-
}
|
|
70908
|
-
sameConfig(cfg) {
|
|
70909
|
-
const c = this.cfg;
|
|
70910
|
-
if (c === void 0) return false;
|
|
70911
|
-
return c.modelPath === cfg.modelPath && c.mmprojPath === cfg.mmprojPath && c.contextSize === cfg.contextSize && c.gpuLayers === cfg.gpuLayers && c.threads === cfg.threads && c.parallel === cfg.parallel && c.batchSize === cfg.batchSize && c.ubatchSize === cfg.ubatchSize && c.flashAttention === cfg.flashAttention && c.mlock === cfg.mlock && c.noMmap === cfg.noMmap && c.cacheTypeK === cfg.cacheTypeK && c.cacheTypeV === cfg.cacheTypeV;
|
|
70912
|
-
}
|
|
70913
|
-
async start(cfg) {
|
|
70914
|
-
if (this.state === "ready" && this.sameConfig(cfg)) return this.status();
|
|
70915
|
-
if (this.child !== void 0) await this.stop();
|
|
70916
|
-
this.cfg = cfg;
|
|
70917
|
-
this.lastError = void 0;
|
|
70918
|
-
this.intentionalStop = false;
|
|
70919
|
-
this.crashPolicy.reset(this.supervisorId);
|
|
70920
|
-
await this.spawnChild(cfg);
|
|
70921
|
-
return this.status();
|
|
70922
|
-
}
|
|
70923
|
-
async spawnChild(cfg) {
|
|
70924
|
-
const port = await this.pickPort();
|
|
70925
|
-
this._port = port;
|
|
70926
|
-
this.state = "starting";
|
|
70927
|
-
const child = this.spawnFn(cfg.binaryPath, buildLlamaArgs(cfg, port), { stdio: "pipe" });
|
|
70928
|
-
this.child = child;
|
|
70929
|
-
child.on("error", (err) => {
|
|
70930
|
-
this.deps.logger.warn("llama-server error event", { meta: { error: String(err) } });
|
|
70931
|
-
});
|
|
70932
|
-
child.stdout?.on("data", (chunk) => this.logLines(chunk, false));
|
|
70933
|
-
child.stderr?.on("data", (chunk) => this.logLines(chunk, true));
|
|
70934
|
-
child.on("exit", (code, signal) => this.onExit(code, signal));
|
|
70935
|
-
this.beginHealthGate(port);
|
|
70936
|
-
}
|
|
70937
|
-
logLines(chunk, isErr) {
|
|
70938
|
-
for (const line of chunk.toString().split("\n")) {
|
|
70939
|
-
const trimmed = line.trim();
|
|
70940
|
-
if (trimmed.length === 0) continue;
|
|
70941
|
-
if (isErr) this.deps.logger.warn(trimmed);
|
|
70942
|
-
else this.deps.logger.debug(trimmed);
|
|
70943
|
-
}
|
|
70944
|
-
}
|
|
70945
|
-
beginHealthGate(port) {
|
|
70946
|
-
const deadlineAt = this.now() + this.startTimeoutMs;
|
|
70947
|
-
const poll = async () => {
|
|
70948
|
-
if (this.state !== "starting") return;
|
|
70949
|
-
if (this.now() >= deadlineAt) {
|
|
70950
|
-
this.lastError = `health gate timed out after ${this.startTimeoutMs}ms`;
|
|
70951
|
-
this.deps.logger.warn(this.lastError);
|
|
70952
|
-
this.intentionalStop = true;
|
|
70953
|
-
this.killChild("SIGKILL");
|
|
70954
|
-
this.state = "crashed";
|
|
70955
|
-
return;
|
|
70956
|
-
}
|
|
70957
|
-
const healthy = await this.probeHealth(port);
|
|
70958
|
-
if (this.state !== "starting") return;
|
|
70959
|
-
if (healthy) {
|
|
70960
|
-
this.onReady();
|
|
70961
|
-
return;
|
|
70962
|
-
}
|
|
70963
|
-
this.healthGateTimer = setTimeout(() => void poll(), HEALTH_GATE_INTERVAL_MS);
|
|
70964
|
-
this.healthGateTimer.unref?.();
|
|
70965
|
-
};
|
|
70966
|
-
poll();
|
|
70967
|
-
}
|
|
70968
|
-
async probeHealth(port) {
|
|
70969
|
-
try {
|
|
70970
|
-
return (await this.fetchFn(`http://127.0.0.1:${port}/health`)).status === 200;
|
|
70971
|
-
} catch {
|
|
70972
|
-
return false;
|
|
70973
|
-
}
|
|
70974
|
-
}
|
|
70975
|
-
onReady() {
|
|
70976
|
-
this.clearStartTimers();
|
|
70977
|
-
this.state = "ready";
|
|
70978
|
-
this.lastError = void 0;
|
|
70979
|
-
this.noteActivity();
|
|
70980
|
-
this.startHealthPoll();
|
|
70981
|
-
this.startIdleWatch();
|
|
70982
|
-
}
|
|
70983
|
-
startHealthPoll() {
|
|
70984
|
-
this.healthPollTimer = setInterval(() => {
|
|
70985
|
-
if (this.state !== "ready" || this._port === void 0) return;
|
|
70986
|
-
this.probeHealth(this._port).then((healthy) => {
|
|
70987
|
-
if (!healthy && this.state === "ready") {
|
|
70988
|
-
this.deps.logger.warn("llama-server health poll failed → treating as crash");
|
|
70989
|
-
this.killChild("SIGKILL");
|
|
70990
|
-
}
|
|
70991
|
-
});
|
|
70992
|
-
}, this.healthPollMs);
|
|
70993
|
-
this.healthPollTimer.unref?.();
|
|
70994
|
-
}
|
|
70995
|
-
startIdleWatch() {
|
|
70996
|
-
const cfg = this.cfg;
|
|
70997
|
-
if (cfg === void 0 || cfg.idleStopMinutes <= 0) return;
|
|
70998
|
-
const idleMs = cfg.idleStopMinutes * 6e4;
|
|
70999
|
-
this.idleTimer = setInterval(() => {
|
|
71000
|
-
if (this.state !== "ready") return;
|
|
71001
|
-
if (this.now() - this.lastActivity >= idleMs) {
|
|
71002
|
-
this.deps.logger.info("llama-server idle-stop");
|
|
71003
|
-
this.stop();
|
|
71004
|
-
}
|
|
71005
|
-
}, Math.min(idleMs, 3e4));
|
|
71006
|
-
this.idleTimer.unref?.();
|
|
71007
|
-
}
|
|
71008
|
-
onExit(code, signal) {
|
|
71009
|
-
this.clearAllTimers();
|
|
71010
|
-
this.child = void 0;
|
|
71011
|
-
if (this.intentionalStop) {
|
|
71012
|
-
this.state = "stopped";
|
|
71013
|
-
return;
|
|
71014
|
-
}
|
|
71015
|
-
this.deps.logger.warn("llama-server exited unexpectedly", { meta: {
|
|
71016
|
-
code,
|
|
71017
|
-
signal
|
|
71018
|
-
} });
|
|
71019
|
-
const decision = this.crashPolicy.recordCrash(this.supervisorId, this.now());
|
|
71020
|
-
if (decision.action === "failed") {
|
|
71021
|
-
this.state = "failed";
|
|
71022
|
-
this.lastError = `crash breaker tripped (${decision.crashesInWindow} crashes)`;
|
|
71023
|
-
return;
|
|
71024
|
-
}
|
|
71025
|
-
this.state = "crashed";
|
|
71026
|
-
const cfg = this.cfg;
|
|
71027
|
-
if (cfg === void 0) return;
|
|
71028
|
-
setTimeout(() => {
|
|
71029
|
-
if (this.state !== "crashed") return;
|
|
71030
|
-
this.spawnChild(cfg).catch((err) => {
|
|
71031
|
-
this.lastError = err instanceof Error ? err.message : String(err);
|
|
71032
|
-
this.state = "crashed";
|
|
71033
|
-
});
|
|
71034
|
-
}, decision.backoffMs).unref?.();
|
|
71035
|
-
}
|
|
71036
|
-
killChild(signal) {
|
|
71037
|
-
this.child?.kill(signal);
|
|
71038
|
-
}
|
|
71039
|
-
async stop() {
|
|
71040
|
-
this.intentionalStop = true;
|
|
71041
|
-
this.clearAllTimers();
|
|
71042
|
-
this.crashPolicy.reset(this.supervisorId);
|
|
71043
|
-
const child = this.child;
|
|
71044
|
-
if (child === void 0) {
|
|
71045
|
-
this.state = "stopped";
|
|
71046
|
-
return;
|
|
71047
|
-
}
|
|
71048
|
-
child.kill("SIGTERM");
|
|
71049
|
-
await new Promise((resolve) => {
|
|
71050
|
-
const grace = setTimeout(() => {
|
|
71051
|
-
child.kill("SIGKILL");
|
|
71052
|
-
resolve();
|
|
71053
|
-
}, this.killGraceMs);
|
|
71054
|
-
grace.unref?.();
|
|
71055
|
-
child.once("exit", () => {
|
|
71056
|
-
clearTimeout(grace);
|
|
71057
|
-
resolve();
|
|
71058
|
-
});
|
|
71059
|
-
});
|
|
71060
|
-
this.child = void 0;
|
|
71061
|
-
this.state = "stopped";
|
|
71062
|
-
}
|
|
71063
|
-
clearStartTimers() {
|
|
71064
|
-
if (this.healthGateTimer !== void 0) clearTimeout(this.healthGateTimer);
|
|
71065
|
-
if (this.startDeadlineTimer !== void 0) clearTimeout(this.startDeadlineTimer);
|
|
71066
|
-
this.healthGateTimer = void 0;
|
|
71067
|
-
this.startDeadlineTimer = void 0;
|
|
71068
|
-
}
|
|
71069
|
-
clearAllTimers() {
|
|
71070
|
-
this.clearStartTimers();
|
|
71071
|
-
if (this.healthPollTimer !== void 0) clearInterval(this.healthPollTimer);
|
|
71072
|
-
if (this.idleTimer !== void 0) clearInterval(this.idleTimer);
|
|
71073
|
-
this.healthPollTimer = void 0;
|
|
71074
|
-
this.idleTimer = void 0;
|
|
71075
|
-
}
|
|
71076
|
-
};
|
|
71077
|
-
//#endregion
|
|
71078
72325
|
//#region src/settings-store-port.ts
|
|
71079
72326
|
function createApiSettingsStorePort(api) {
|
|
71080
72327
|
return {
|
|
@@ -71582,10 +72829,27 @@ async function runTestChatStream(deps, request, emit, signal) {
|
|
|
71582
72829
|
kind: "status",
|
|
71583
72830
|
phase: "connecting"
|
|
71584
72831
|
});
|
|
72832
|
+
let modelWaitStartedAt = null;
|
|
71585
72833
|
const opened = await deps.openStream(streamRequest, {
|
|
71586
72834
|
signal,
|
|
71587
|
-
connectTimeoutMs: TEST_CHAT_CONNECT_TIMEOUT_MS
|
|
72835
|
+
connectTimeoutMs: TEST_CHAT_CONNECT_TIMEOUT_MS,
|
|
72836
|
+
firstTokenTimeoutMs: request.firstTokenTimeoutMs,
|
|
72837
|
+
onConnected: () => {
|
|
72838
|
+
modelWaitStartedAt = deps.now();
|
|
72839
|
+
emit({
|
|
72840
|
+
kind: "status",
|
|
72841
|
+
phase: "first-token-wait"
|
|
72842
|
+
});
|
|
72843
|
+
}
|
|
71588
72844
|
});
|
|
72845
|
+
/**
|
|
72846
|
+
* What is LEFT of the first-token budget.
|
|
72847
|
+
*
|
|
72848
|
+
* The response headers and the first token are one wait spent two ways, so
|
|
72849
|
+
* they draw on one budget: the page says "up to Ns" and that has to be the
|
|
72850
|
+
* whole truth, not N per stage.
|
|
72851
|
+
*/
|
|
72852
|
+
const remainingFirstTokenMs = () => modelWaitStartedAt === null ? request.firstTokenTimeoutMs : Math.max(1, request.firstTokenTimeoutMs - (deps.now() - modelWaitStartedAt));
|
|
71589
72853
|
if (opened.kind === "connect-timeout") {
|
|
71590
72854
|
await fail({
|
|
71591
72855
|
code: "unavailable",
|
|
@@ -71606,6 +72870,20 @@ async function runTestChatStream(deps, request, emit, signal) {
|
|
|
71606
72870
|
});
|
|
71607
72871
|
return;
|
|
71608
72872
|
}
|
|
72873
|
+
if (opened.kind === "provider-error") {
|
|
72874
|
+
await fail({
|
|
72875
|
+
code: opened.code,
|
|
72876
|
+
message: opened.message
|
|
72877
|
+
}, "ai test chat: provider refused the request — turn dropped", {
|
|
72878
|
+
code: opened.code,
|
|
72879
|
+
error: opened.message
|
|
72880
|
+
});
|
|
72881
|
+
return;
|
|
72882
|
+
}
|
|
72883
|
+
if (opened.kind === "first-token-timeout") {
|
|
72884
|
+
await failFirstToken();
|
|
72885
|
+
return;
|
|
72886
|
+
}
|
|
71609
72887
|
const streamed = opened.kind === "open";
|
|
71610
72888
|
emit({
|
|
71611
72889
|
kind: "meta",
|
|
@@ -71631,7 +72909,7 @@ async function runTestChatStream(deps, request, emit, signal) {
|
|
|
71631
72909
|
kind: "status",
|
|
71632
72910
|
phase: "first-token-wait"
|
|
71633
72911
|
});
|
|
71634
|
-
const raced = await withDeadline(deps.generateOnce(streamRequest),
|
|
72912
|
+
const raced = await withDeadline(deps.generateOnce(streamRequest), remainingFirstTokenMs());
|
|
71635
72913
|
if (signal.aborted) {
|
|
71636
72914
|
deps.logger.info("ai test chat: client aborted mid-generation", withTags({}));
|
|
71637
72915
|
return;
|
|
@@ -71689,7 +72967,7 @@ async function runTestChatStream(deps, request, emit, signal) {
|
|
|
71689
72967
|
deps.logger.info("ai test chat: client aborted mid-stream — provider call torn down", { ...withTags({ sawFirstToken }) });
|
|
71690
72968
|
return;
|
|
71691
72969
|
}
|
|
71692
|
-
const next = await nextChunk(iterator, sawFirstToken ? TEST_CHAT_IDLE_TIMEOUT_MS :
|
|
72970
|
+
const next = await nextChunk(iterator, sawFirstToken ? TEST_CHAT_IDLE_TIMEOUT_MS : remainingFirstTokenMs());
|
|
71693
72971
|
if (next.kind === "timeout") {
|
|
71694
72972
|
if (!sawFirstToken) {
|
|
71695
72973
|
await failFirstToken();
|
|
@@ -71861,7 +73139,7 @@ var AiAddon = class extends BaseAddon {
|
|
|
71861
73139
|
const settingsPort = api !== void 0 ? createApiSettingsStorePort(api) : createMemorySettingsStorePort();
|
|
71862
73140
|
if (api === void 0) logger.warn("addon-ai: no ctx.api — profiles are in-memory only");
|
|
71863
73141
|
const binDir = path$1.join(ctx.nodeDataDir, "bin");
|
|
71864
|
-
const { ensureLlamaServer } = await import("./ensure-llama-server-
|
|
73142
|
+
const { ensureLlamaServer } = await import("./ensure-llama-server-COC6iveo.mjs").then((n) => n.r);
|
|
71865
73143
|
const assembly = await assembleAi({
|
|
71866
73144
|
nodeId: ownNodeId,
|
|
71867
73145
|
isHub,
|
|
@@ -71949,7 +73227,11 @@ var AiAddon = class extends BaseAddon {
|
|
|
71949
73227
|
...request.maxTokens !== void 0 ? { maxTokens: request.maxTokens } : {},
|
|
71950
73228
|
...request.temperature !== void 0 ? { temperature: request.temperature } : {},
|
|
71951
73229
|
signal: opts.signal
|
|
71952
|
-
}, {
|
|
73230
|
+
}, {
|
|
73231
|
+
connectTimeoutMs: opts.connectTimeoutMs,
|
|
73232
|
+
firstTokenTimeoutMs: opts.firstTokenTimeoutMs,
|
|
73233
|
+
onConnected: opts.onConnected
|
|
73234
|
+
}),
|
|
71953
73235
|
generateOnce: async (request) => {
|
|
71954
73236
|
const base = {
|
|
71955
73237
|
profileId: request.profile.id,
|
|
@@ -72018,4 +73300,4 @@ var AiAddon = class extends BaseAddon {
|
|
|
72018
73300
|
}
|
|
72019
73301
|
};
|
|
72020
73302
|
//#endregion
|
|
72021
|
-
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,
|
|
73303
|
+
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 };
|