@camstack/addon-ai 0.2.20 → 0.4.1

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.mjs CHANGED
@@ -7,7 +7,7 @@ import { brotliCompress, gzip } from "node:zlib";
7
7
  import * as fsp from "node:fs/promises";
8
8
  import { spawn } from "node:child_process";
9
9
  import { createServer } from "node:net";
10
- //#region ../types/dist/event-category-Cv9dO26A.mjs
10
+ //#region ../types/dist/event-category-Bxo5yJjt.mjs
11
11
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
12
12
  EventCategory["SystemBoot"] = "system.boot";
13
13
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -214,6 +214,33 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
214
214
  EventCategory["PipelineCameraAssigned"] = "pipeline.camera-assigned";
215
215
  EventCategory["PipelineCameraUnassigned"] = "pipeline.camera-unassigned";
216
216
  /**
217
+ * A node the orchestrator would otherwise place cameras on has NO usable
218
+ * inference device: the operator enabled one or more accelerators there and
219
+ * the live probe reports every one of them unavailable. Emitted once per
220
+ * TRANSITION into that state (never per dispatch), and the node is dropped
221
+ * from the placement candidate set for as long as it holds.
222
+ *
223
+ * This exists because the state was previously invisible: little-unraid
224
+ * absorbed 283k inference errors in a day while still being handed cameras,
225
+ * and nothing in the system said so.
226
+ *
227
+ * A node with no accelerators configured at all is NOT this — its devices
228
+ * are `disabled`, not `unavailable`, and the runner's default CPU pool
229
+ * serves it exactly as before.
230
+ */
231
+ EventCategory["PipelineNodeInferenceUnavailable"] = "pipeline.node-inference-unavailable";
232
+ /**
233
+ * A camera has an OPEN detection session and has produced no detection at
234
+ * all for longer than the blind threshold — the camera is being decoded and
235
+ * inferred and is returning nothing. Emitted once per transition into blind,
236
+ * per camera.
237
+ *
238
+ * The failure it reports: a 1h43 detection blackout on the entrance camera
239
+ * that nobody noticed, because "a camera that detects nothing" and "a quiet
240
+ * camera" produce byte-identical silence.
241
+ */
242
+ EventCategory["PipelineDetectionBlind"] = "pipeline.detection-blind";
243
+ /**
217
244
  * Per-camera pipeline config was mutated by the orchestrator
218
245
  * (3-level settings change via `setAgentAddonDefaults` /
219
246
  * `setCameraStepToggle` / `setCameraPipelineForAgent` or a
@@ -3005,6 +3032,9 @@ function handlePipeResult(left, next, ctx) {
3005
3032
  fallback: left.fallback
3006
3033
  }, ctx);
3007
3034
  }
3035
+ var $ZodPreprocess = /*@__PURE__*/ $constructor("$ZodPreprocess", (inst, def) => {
3036
+ $ZodPipe.init(inst, def);
3037
+ });
3008
3038
  var $ZodReadonly = /*@__PURE__*/ $constructor("$ZodReadonly", (inst, def) => {
3009
3039
  $ZodType.init(inst, def);
3010
3040
  defineLazy(inst._zod, "propValues", () => def.innerType._zod.propValues);
@@ -4322,7 +4352,7 @@ var parseAsync = /* @__PURE__ */ _parseAsync(ZodRealError);
4322
4352
  var safeParse = /* @__PURE__ */ _safeParse(ZodRealError);
4323
4353
  var safeParseAsync = /* @__PURE__ */ _safeParseAsync(ZodRealError);
4324
4354
  var encode = /* @__PURE__ */ _encode(ZodRealError);
4325
- var decode = /* @__PURE__ */ _decode(ZodRealError);
4355
+ var decode$1 = /* @__PURE__ */ _decode(ZodRealError);
4326
4356
  var encodeAsync = /* @__PURE__ */ _encodeAsync(ZodRealError);
4327
4357
  var decodeAsync = /* @__PURE__ */ _decodeAsync(ZodRealError);
4328
4358
  var safeEncode = /* @__PURE__ */ _safeEncode(ZodRealError);
@@ -4383,7 +4413,7 @@ var ZodType = /*@__PURE__*/ $constructor("ZodType", (inst, def) => {
4383
4413
  inst.safeParseAsync = async (data, params) => safeParseAsync(inst, data, params);
4384
4414
  inst.spa = inst.safeParseAsync;
4385
4415
  inst.encode = (data, params) => encode(inst, data, params);
4386
- inst.decode = (data, params) => decode(inst, data, params);
4416
+ inst.decode = (data, params) => decode$1(inst, data, params);
4387
4417
  inst.encodeAsync = async (data, params) => encodeAsync(inst, data, params);
4388
4418
  inst.decodeAsync = async (data, params) => decodeAsync(inst, data, params);
4389
4419
  inst.safeEncode = (data, params) => safeEncode(inst, data, params);
@@ -5190,6 +5220,10 @@ function pipe(in_, out) {
5190
5220
  out
5191
5221
  });
5192
5222
  }
5223
+ var ZodPreprocess = /*@__PURE__*/ $constructor("ZodPreprocess", (inst, def) => {
5224
+ ZodPipe.init(inst, def);
5225
+ $ZodPreprocess.init(inst, def);
5226
+ });
5193
5227
  var ZodReadonly = /*@__PURE__*/ $constructor("ZodReadonly", (inst, def) => {
5194
5228
  $ZodReadonly.init(inst, def);
5195
5229
  ZodType.init(inst, def);
@@ -5248,6 +5282,13 @@ function _instanceof(cls, params = {}) {
5248
5282
  };
5249
5283
  return inst;
5250
5284
  }
5285
+ function preprocess(fn, schema) {
5286
+ return new ZodPreprocess({
5287
+ type: "pipe",
5288
+ in: transform(fn),
5289
+ out: schema
5290
+ });
5291
+ }
5251
5292
  //#endregion
5252
5293
  //#region ../../node_modules/zod/v4/classic/compat.js
5253
5294
  /** @deprecated Use the raw string literal codes instead, e.g. "invalid_type". */
@@ -5364,6 +5405,12 @@ Object.fromEntries([
5364
5405
  icon: "shapes",
5365
5406
  order: 38
5366
5407
  },
5408
+ {
5409
+ id: "scenes",
5410
+ label: "Scenes",
5411
+ icon: "scan-eye",
5412
+ order: 36
5413
+ },
5367
5414
  {
5368
5415
  id: "analytics",
5369
5416
  label: "Analytics",
@@ -13033,11 +13080,33 @@ var NotificationFormatSchema = _enum([
13033
13080
  * Named by INTENT, never by glyph. "check" would tie the vocabulary to one
13034
13081
  * renderer's icon set; "acknowledge" survives an adapter that draws it
13035
13082
  * differently.
13083
+ *
13084
+ * ── A TOKEN IS NOT A WIRE VALUE ─────────────────────────────────────
13085
+ *
13086
+ * These names are for US. **No adapter may forward one verbatim.** Each maps
13087
+ * the whole set onto its own renderer's vocabulary through a
13088
+ * `Record<NotificationActionIcon, string>` — a Record, never a lookup with a
13089
+ * fallback, so adding a member here fails every adapter's build until someone
13090
+ * decides its glyph, which is the only place that decision can be made
13091
+ * honestly.
13092
+ *
13093
+ * This paragraph is the bug. Zentik declared `actionIcons: true` and passed
13094
+ * `disarm` straight through; iOS feeds that string to
13095
+ * `UNNotificationActionIcon(systemImageName:)`, `disarm` is not an SF Symbol,
13096
+ * and every snooze and alarm button arrived BLANK. A pass-through is not a
13097
+ * mapping, and "the field is documented" is not "the value renders".
13098
+ *
13099
+ * Adding a member is TRAIN-BOUND. The enum lives in the published
13100
+ * `@camstack/server` closure and the cap seam validates against the HUB's copy,
13101
+ * so an addon that emits a token the running hub does not know does not lose an
13102
+ * icon — its whole `send` fails Zod validation and the notification never
13103
+ * arrives. Never emit a new token from an addon before the train carrying it.
13036
13104
  */
13037
13105
  var NotificationActionIconSchema = _enum([
13038
13106
  "acknowledge",
13039
13107
  "dismiss",
13040
13108
  "silence",
13109
+ "snooze",
13041
13110
  "view",
13042
13111
  "play",
13043
13112
  "open",
@@ -13045,9 +13114,13 @@ var NotificationActionIconSchema = _enum([
13045
13114
  "lock",
13046
13115
  "unlock",
13047
13116
  "arm",
13117
+ "arm-home",
13118
+ "arm-away",
13119
+ "arm-night",
13048
13120
  "disarm",
13049
13121
  "light",
13050
- "alert"
13122
+ "alert",
13123
+ "camera"
13051
13124
  ]);
13052
13125
  /** A single tap-through action button. */
13053
13126
  var NotificationActionSchema = object({
@@ -13247,6 +13320,24 @@ method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSche
13247
13320
  targetId: string(),
13248
13321
  enabled: boolean()
13249
13322
  }), _void(), { kind: "mutation" });
13323
+ new Set([
13324
+ {
13325
+ id: "person",
13326
+ name: "Person"
13327
+ },
13328
+ {
13329
+ id: "vehicle",
13330
+ name: "Vehicle"
13331
+ },
13332
+ {
13333
+ id: "animal",
13334
+ name: "Animal"
13335
+ },
13336
+ {
13337
+ id: "package",
13338
+ name: "Package"
13339
+ }
13340
+ ].map((l) => l.id));
13250
13341
  var COCO_TO_MACRO = {
13251
13342
  mapping: {
13252
13343
  person: "person",
@@ -14039,11 +14130,15 @@ var NcSystemEventKindSchema = _enum([
14039
14130
  "stream-offline",
14040
14131
  "node-online",
14041
14132
  "node-offline",
14133
+ "node-inference-unavailable",
14134
+ "detection-blind",
14042
14135
  "addon-update-available",
14043
14136
  "server-update-available",
14044
14137
  "alarm-triggered",
14045
14138
  "alarm-armed",
14046
14139
  "alarm-disarmed",
14140
+ "alarm-arming",
14141
+ "alarm-arm-refused",
14047
14142
  "camera-online",
14048
14143
  "camera-offline",
14049
14144
  "camera-disabled",
@@ -14098,7 +14193,16 @@ var NcScheduleSchema = object({
14098
14193
  });
14099
14194
  /** Fuzzy plate matcher — OCR noise makes exact match useless (spec row 12/13). */
14100
14195
  var NcPlateMatcherSchema = object({
14101
- values: array(string().min(1)).min(1),
14196
+ /**
14197
+ * Plate texts (or gallery vehicle names) to match. EMPTY = **any plate the
14198
+ * pipeline could read** — the plate half of "no selection = no narrowing",
14199
+ * and the switch that says this rule is about vehicles that were IDENTIFIED
14200
+ * rather than merely seen. A subject carrying no plate still fails.
14201
+ *
14202
+ * The `.min(1)` this used to carry made that state unauthorable; nothing has
14203
+ * ever persisted an empty list, so widening it cannot change an existing rule.
14204
+ */
14205
+ values: array(string().min(1)),
14102
14206
  /** Max Levenshtein distance after normalization (uppercase alphanumeric). */
14103
14207
  maxDistance: number().int().min(0).max(3).default(1)
14104
14208
  });
@@ -14337,18 +14441,47 @@ var NcConditionsSchema = object({
14337
14441
  */
14338
14442
  labelEquals: array(string().min(1)).optional(),
14339
14443
  /**
14340
- * Identity matcher. P1 boundary: matched against the record's collapsed
14341
- * `label` (the identity display name propagated by the face pipeline) —
14342
- * identity-ID matching rides in P2 when identity ids reach the record.
14444
+ * KNOWN FACES the rule's identity scope, and the switch that says the rule
14445
+ * is about recognised people at all.
14446
+ *
14447
+ * Three states, and the empty one is the point:
14448
+ *
14449
+ * | value | meaning |
14450
+ * | --- | --- |
14451
+ * | absent | the rule does not care who it is; an unrecognised person matches |
14452
+ * | `[]` | **only known faces** — any identity in the gallery, nobody in particular |
14453
+ * | a list | only these identities |
14454
+ *
14455
+ * `[]` is the repo-wide "no selection = no narrowing" reading (an absent
14456
+ * `devices` list is every device), applied one level down: the operator has
14457
+ * turned the face scope ON and narrowed it to nothing, which is every known
14458
+ * face. No second field states the same thing — a switch that can disagree
14459
+ * with the list under it is worse than no switch (D62).
14460
+ *
14461
+ * MEMBERS ARE FACE-GALLERY `Identity.id`s (uuid), not display names. A name is
14462
+ * renameable, and a rule authored on "Gianluca" went silently dark the moment
14463
+ * the operator fixed the spelling. The id reaches the record on
14464
+ * `LabelAttribution.identityId`; the name is what the editor shows and what
14465
+ * `{{label}}` renders.
14466
+ *
14467
+ * Rules written before this carry NAMES, and are resolved to ids lazily at
14468
+ * load (`NcRuleStore.load`) against the live gallery — a name nothing answers
14469
+ * for is left as it stands and reported, never dropped. The engine also
14470
+ * accepts a display-name hit as a compatibility leg, so a rule whose
14471
+ * migration could not resolve keeps matching exactly what it matched before.
14343
14472
  */
14344
14473
  identities: array(string().min(1)).optional(),
14345
- /** Fuzzy plate matcher against the record's `label` (plate text). */
14474
+ /**
14475
+ * KNOWN PLATES / VEHICLES — the plate mirror of {@link identities}, including
14476
+ * the empty-list reading: `values: []` is "any plate the OCR could read",
14477
+ * a non-empty list is those plates (fuzzily). See {@link NcPlateMatcherSchema}.
14478
+ */
14346
14479
  plates: NcPlateMatcherSchema.optional(),
14347
14480
  /**
14348
- * Identity EXCLUDE — mirror of {@link identities} with `notIn` semantics.
14349
- * Same P1 boundary: matched against the record's collapsed `label` (the
14350
- * identity display name). A record with NO label passes (nothing to
14351
- * exclude), unlike the include variant which fails on an absent label.
14481
+ * Identity EXCLUDE — mirror of {@link identities} with `notIn` semantics, and
14482
+ * the same id members and the same lazy name→id migration. A record with NO
14483
+ * identity passes (nothing to exclude), unlike the include variant which
14484
+ * fails on an unrecognised subject. An EMPTY list excludes nobody.
14352
14485
  */
14353
14486
  identitiesExclude: array(string().min(1)).optional(),
14354
14487
  /**
@@ -14740,7 +14873,80 @@ var NcRuleInputSchema = object({
14740
14873
  * a rule that predates the gate must keep delivering byte-for-byte as it
14741
14874
  * did, and absent is the only way to say that without a migration.
14742
14875
  */
14743
- confirm: NcConfirmSchema.optional()
14876
+ confirm: NcConfirmSchema.optional(),
14877
+ /**
14878
+ * WAIT for face/plate recognition before saying anything.
14879
+ *
14880
+ * A notification's TEXT is frozen at enqueue and its media is re-resolved at
14881
+ * send; the identity is neither. A face is confirmed after `confirmFrames`
14882
+ * agreeing observations — p50 **11.4 s** after the track was first seen,
14883
+ * measured on this hub — and an `immediate` rule enqueues on the first object
14884
+ * event, seconds before that. So "Gianluca è arrivato" is unsayable on the
14885
+ * immediate path, and no amount of media re-resolution fixes a sentence.
14886
+ *
14887
+ * Only two honest answers exist, and this flag picks between them. It has
14888
+ * effect ONLY on a rule that declares a recognition scope
14889
+ * ({@link NcConditions.identities} or {@link NcConditions.plates}) — on any
14890
+ * other rule there is nothing to wait for and the flag is inert.
14891
+ *
14892
+ * | value | what happens |
14893
+ * | --- | --- |
14894
+ * | `true` | the rule stops firing on the object event and fires at TRACK CLOSE instead, once, with the name — later, and complete |
14895
+ * | absent / `false` | it fires at once WITHOUT the name, and if recognition lands before the track closes a SECOND, "…is Gianluca" notification follows (one per track, per rule, per target) |
14896
+ *
14897
+ * `.optional()` and deliberately NOT `.default()`: a Zod default does not run
14898
+ * on the addon cap path, and absent has to keep meaning exactly what every
14899
+ * rule authored before this field meant.
14900
+ *
14901
+ * The cost of `true` is stated here because the editor states it too: a rule
14902
+ * that waits also inherits track-close SEMANTICS — its `zones` condition
14903
+ * tests every zone the track visited and a `crossing` condition can no longer
14904
+ * be satisfied, because a closed track carries no crossing.
14905
+ */
14906
+ waitForEnhancement: boolean().optional(),
14907
+ /**
14908
+ * GROUP a burst of subjects into ONE notification that grows.
14909
+ *
14910
+ * Seconds of quiet after the last matching subject before the burst is
14911
+ * considered over. While it is open, the first subject enqueues immediately —
14912
+ * **exactly as today, with no added latency** — and every real growth (a new
14913
+ * subject, or a name confirmed on one already in it) REPLACES that
14914
+ * notification with an updated one naming everybody. The push carries the
14915
+ * group's own coalescing tag, so the phone replaces rather than stacks.
14916
+ *
14917
+ * `0` / absent = off, and off is today's behaviour byte for byte.
14918
+ *
14919
+ * ### Why an idle cutoff and not a window
14920
+ *
14921
+ * The measured seven-person arrival on device 590 spans 110 s with every
14922
+ * internal gap under 30 s. A 12 s fixed window cuts it into three groups; an
14923
+ * idle cutoff holds it as one and ends it when the arrival actually ends.
14924
+ * 30 is Frigate's shipped value for the same decision.
14925
+ *
14926
+ * ### What it replaces
14927
+ *
14928
+ * The blind cooldown, which collapses a burst by DISCARDING it. Measured on
14929
+ * device 615 / *Persona su Uscio* over six days: 116 qualifying tracks → 74
14930
+ * notifications, **44 (37.9%) suppressed outright**, 23 of them overlapping a
14931
+ * track that did fire and 7 carrying a confirmed identity nobody heard about.
14932
+ * A group collapses the same volume by MERGING, so the cooldown becomes a
14933
+ * budget over GROUPS — which is what it always meant — and a growth is never
14934
+ * throttled by the window its own first member spent.
14935
+ *
14936
+ * ### Interaction with {@link waitForEnhancement}
14937
+ *
14938
+ * They compose, and the order matters. `waitForEnhancement` defers the rule to
14939
+ * TRACK CLOSE, so with both set the group is opened by the first member to
14940
+ * CLOSE — already carrying its name — and grows as later members close. That
14941
+ * is later, and complete. With grouping alone the group opens on the first
14942
+ * object event and picks up names as they are confirmed, through the growth
14943
+ * path. Neither combination fires twice for one subject.
14944
+ *
14945
+ * `.optional()` and deliberately NOT `.default()`: a Zod default does not run
14946
+ * on the addon cap path, so absent must keep meaning what it meant before this
14947
+ * field existed.
14948
+ */
14949
+ groupIdleSec: number().int().min(0).max(600).optional()
14744
14950
  });
14745
14951
  /**
14746
14952
  * Partial patch for `updateRule` — any subset of the input fields, plus the
@@ -15252,7 +15458,87 @@ var MethodAccessSchema = _enum([
15252
15458
  var AllowedProviderSchema = union([literal("*"), array(string())]);
15253
15459
  var AllowedDevicesSchema = record(string(), union([literal("*"), array(string())]));
15254
15460
  var CapScopeSchema = _enum(["device", "system"]);
15255
- var TokenScopeSchema = discriminatedUnion("type", [
15461
+ /**
15462
+ * DeviceSelector (scope model v3 — 2026-08-12).
15463
+ *
15464
+ * A `device` grant no longer carries a frozen list of deviceIds. It carries
15465
+ * a SELECTOR the matcher resolves against the live fleet, so the grant can be
15466
+ * DYNAMIC: a `types:['camera']` selector automatically covers a camera added
15467
+ * AFTER the grant was minted — no re-grant, no re-login.
15468
+ *
15469
+ * - `all` — every device in the deployment. The broad viewer/operator
15470
+ * lever without a `category` grant (a `category` grant also covers device
15471
+ * caps that carry no deviceId; `all` is specifically the device set).
15472
+ * - `ids` — an explicit deviceId list. This is what a v2 `device:[…]`
15473
+ * grant migrates to (see {@link TokenScopeSchema}); STATIC — a new camera
15474
+ * is NOT covered until the grant is edited.
15475
+ * - `types` — every device of a `DeviceType` (e.g. every `camera`).
15476
+ * DYNAMIC. A device that changes type, or a new device of the type,
15477
+ * re-resolves on the next request.
15478
+ * - `locations` — every device whose operator-assigned `location` label is
15479
+ * in the set (e.g. "Garden", "Front door"). DYNAMIC. A device with a
15480
+ * null/unset location matches NO `locations` selector.
15481
+ */
15482
+ var DeviceSelectorSchema = discriminatedUnion("kind", [
15483
+ object({ kind: literal("all") }),
15484
+ object({
15485
+ kind: literal("ids"),
15486
+ ids: array(number().int()).min(1)
15487
+ }),
15488
+ object({
15489
+ kind: literal("types"),
15490
+ types: array(_enum(DeviceType)).min(1)
15491
+ }),
15492
+ object({
15493
+ kind: literal("locations"),
15494
+ locations: array(string().min(1)).min(1)
15495
+ })
15496
+ ]);
15497
+ var DeviceTokenScopeSchema = object({
15498
+ type: literal("device"),
15499
+ /** The device SET this grant covers — resolved against the live fleet. */
15500
+ selector: DeviceSelectorSchema,
15501
+ access: array(MethodAccessSchema).min(1),
15502
+ /**
15503
+ * Whether a grant on a PARENT device transparently covers its accessory
15504
+ * CHILDREN (siren / floodlight / PIR) via the persisted-parentage walk.
15505
+ * Direction is parent → children ONLY.
15506
+ *
15507
+ * Absent → the matcher DERIVES it from the access flavour: `view`
15508
+ * inherits (a camera viewer sees the camera's accessories), `create` /
15509
+ * `delete` do NOT (actuating/removing a child is an explicit act the
15510
+ * operator must grant on the child, not inherit from the parent). Set it
15511
+ * explicitly to override that default per grant.
15512
+ */
15513
+ includeLinked: boolean().optional()
15514
+ });
15515
+ /**
15516
+ * v2 → v3 lazy migration. A pre-v3 `device` grant carried
15517
+ * `targets: string[]` (stringified deviceIds); it rewrites to the equivalent
15518
+ * `selector: {kind:'ids', ids}`. Applied as a `preprocess` so it runs on
15519
+ * EVERY parse path — stored records AND the JWT-carried scope arrays
15520
+ * normalised at the request boundary ({@link normalizeTokenScopes} in
15521
+ * `device-selector.ts`). Chosen over a one-time DB migration because a
15522
+ * migration cannot reach a JWT already in a client's hands; parse-time
15523
+ * migration covers both without a flag day. No cast — the raw object is read
15524
+ * through `Reflect.get` (its static type is `unknown`).
15525
+ */
15526
+ function migrateLegacyTokenScope(raw) {
15527
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return raw;
15528
+ if (Reflect.get(raw, "type") !== "device") return raw;
15529
+ if (Reflect.get(raw, "selector") !== void 0) return raw;
15530
+ const targets = Reflect.get(raw, "targets");
15531
+ if (!Array.isArray(targets)) return raw;
15532
+ return {
15533
+ type: "device",
15534
+ selector: {
15535
+ kind: "ids",
15536
+ ids: targets.map((t) => typeof t === "string" ? Number(t) : t).filter((n) => typeof n === "number" && Number.isInteger(n))
15537
+ },
15538
+ access: Reflect.get(raw, "access")
15539
+ };
15540
+ }
15541
+ var TokenScopeSchema = preprocess(migrateLegacyTokenScope, discriminatedUnion("type", [
15256
15542
  object({
15257
15543
  type: literal("category"),
15258
15544
  target: CapScopeSchema,
@@ -15268,18 +15554,8 @@ var TokenScopeSchema = discriminatedUnion("type", [
15268
15554
  target: string(),
15269
15555
  access: array(MethodAccessSchema).min(1)
15270
15556
  }),
15271
- object({
15272
- type: literal("device"),
15273
- /**
15274
- * One or more deviceIds (serialised as strings for wire-format
15275
- * consistency with the rest of the union). Matcher accepts if
15276
- * `input.deviceId` ∈ `targets`. Array shape avoids the row-explosion
15277
- * of one scope-per-device when granting access to a set of cameras.
15278
- */
15279
- targets: array(string()).min(1),
15280
- access: array(MethodAccessSchema).min(1)
15281
- })
15282
- ]);
15557
+ DeviceTokenScopeSchema
15558
+ ]));
15283
15559
  object({
15284
15560
  id: string(),
15285
15561
  username: string(),
@@ -15596,7 +15872,7 @@ var TrackEnvelopeSchema = object({
15596
15872
  * `snapshots[]` references — megabytes across a page of tracks. `slim`
15597
15873
  * keeps every scalar the list surfaces actually render (ids, class(es),
15598
15874
  * label / audioLabels / importance enrichment, firstSeen/lastSeen, state,
15599
- * zonesVisited, bestEventId, envelope, hasFace) and returns `positions` /
15875
+ * zonesVisited, bestEventId, envelope, hasFace, hasRider) and returns `positions` /
15600
15876
  * `snapshots` as EMPTY arrays — detail views re-fetch the full row via
15601
15877
  * `getTrack`. Mirrors the event-store `projection` convention
15602
15878
  * (`getObjectEvents` et al.).
@@ -15732,7 +16008,21 @@ union([literal(1), literal(2)]);
15732
16008
  var LabelAttributionSchema = object({
15733
16009
  stepId: string(),
15734
16010
  modelId: string().optional(),
15735
- decidedAt: number()
16011
+ decidedAt: number(),
16012
+ /**
16013
+ * The GALLERY id behind a recognised tier-2 label — a face-gallery
16014
+ * `Identity.id` or a plate-gallery `Vehicle.id` (both `randomUUID`).
16015
+ *
16016
+ * The text alone is a DISPLAY NAME, and a display name is renameable: a
16017
+ * notification rule authored on "Gianluca" stopped matching the moment the
16018
+ * operator fixed the spelling in the gallery, and nothing said so. The id is
16019
+ * the thing that does not move, so it is what a rule matches on
16020
+ * (`NcConditions.identities`) and the text is what a human is shown.
16021
+ *
16022
+ * Absent when the label names no gallery row — a plate the OCR read but no
16023
+ * vehicle claims, a sub-class, a species, any tier-1 value.
16024
+ */
16025
+ identityId: string().optional()
15736
16026
  });
15737
16027
  /**
15738
16028
  * The TIERED label model (roadmap 4g), spread into `TrackSchema` and
@@ -15869,6 +16159,28 @@ var TrackSchema = object({
15869
16159
  * `=== true` and render nothing otherwise, never infer "no face".
15870
16160
  */
15871
16161
  hasFace: boolean().optional(),
16162
+ /**
16163
+ * This subject CONTAINS a folded rider — a person the rider-pairing step
16164
+ * ([D34](../decisions/adr-0034.md)) removed from the frame BEFORE the tracker,
16165
+ * so the passage is tracked once and as a VEHICLE.
16166
+ *
16167
+ * It exists because the fold's record was dishonest. D34 and the code both
16168
+ * said "the person is not lost — it is reported so both entities stay on the
16169
+ * record"; in fact the pair went into a per-processor RAM field behind an
16170
+ * accessor nobody called, and every durable surface said `vehicle`, full
16171
+ * stop. This is the composition note that makes the row true.
16172
+ *
16173
+ * A COMPOSITION, never a class and never a label. "This vehicle contains a
16174
+ * person" is not an answer to "what is this" — both label tiers would refuse
16175
+ * a macro token anyway (D89), and correctly. Nothing here changes what the
16176
+ * subject IS: a cyclist stays one vehicle track, occupancy still counts one,
16177
+ * and a `person` rule still does not fire for someone cycling past.
16178
+ *
16179
+ * **Absent ≠ false**, exactly like {@link hasFace}: every row written before
16180
+ * the column, and every hub that predates the field, omits it. Test
16181
+ * `=== true` and render nothing otherwise — never infer "no rider".
16182
+ */
16183
+ hasRider: boolean().optional(),
15872
16184
  ...TrackFlagFields,
15873
16185
  ...TrackRetrainFields
15874
16186
  });
@@ -17276,10 +17588,26 @@ var detectionFpsField = {
17276
17588
  default: 10,
17277
17589
  step: 1
17278
17590
  };
17591
+ /**
17592
+ * The occupancy re-check interval. DEFAULT 300 s (2026-08-13 — was 30 s).
17593
+ *
17594
+ * The recheck is now on by default (a parked car is invisible to occupancy
17595
+ * rules until the stationary registry has been rebuilt by motion, which after a
17596
+ * restart may be never on a quiet camera). Each cycle re-subscribes a detection
17597
+ * session — an RTSP re-dial — so the switch is only affordable at a WIDE
17598
+ * interval: 300 s is ~12 re-dials an hour per camera, against 120 at the old
17599
+ * 30 s. A parked car is therefore counted within 5 minutes of a restart.
17600
+ *
17601
+ * Why not wider: `max` is 300 and raising it is TRAIN-BOUND, not addon-bound —
17602
+ * the host validates `attachCamera` against ITS copy of this schema, so a
17603
+ * runner asked for 600 would be rejected by the hub until a `@camstack/server`
17604
+ * carrying the wider bound is installed everywhere. 300 is the widest value
17605
+ * that ships with an addon deploy.
17606
+ */
17279
17607
  var occupancyRecheckSecField = {
17280
17608
  min: 0,
17281
17609
  max: 300,
17282
- default: 30,
17610
+ default: 300,
17283
17611
  step: 5
17284
17612
  };
17285
17613
  var occupancyRecheckFramesField = {
@@ -17477,15 +17805,21 @@ var RunnerCameraConfigSchema = object({
17477
17805
  */
17478
17806
  onboardMotionDrivesAnalyzer: boolean().default(true),
17479
17807
  /**
17480
- * Master toggle for the occupancy re-check. When `false` (DEFAULT) the runner
17481
- * never arms the periodic recheck timer, regardless of `occupancyRecheckSec`
17482
- * this is off by default because the recheck re-subscribes a detection session
17483
- * every N seconds while `watching`, a major source of pull-decoder re-dial
17484
- * churn (each cycle creates+tears a session → RTSP re-dial → latency). The
17808
+ * Master toggle for the occupancy re-check. When `false` the runner never arms
17809
+ * the periodic recheck timer, regardless of `occupancyRecheckSec`; the
17485
17810
  * `occupancyRecheckSec` / `occupancyRecheckFrames` sliders only take effect
17486
17811
  * (and only render) when this is enabled.
17487
- */
17488
- occupancyRecheckEnabled: boolean().default(false),
17812
+ *
17813
+ * DEFAULT `true` since 2026-08-13 (was `false`). It was off because the
17814
+ * recheck re-subscribes a detection session every N seconds while `watching`
17815
+ * — each cycle creates+tears a session ⇒ an RTSP re-dial ⇒ latency, a major
17816
+ * pull-decoder churn source. What that bought was a blind spot: a STATIONARY
17817
+ * object is counted only while the stationary registry holds it, and the
17818
+ * registry rebuilds from motion, so after a restart a parked car was invisible
17819
+ * to every occupancy rule until something moved in front of it. The churn is
17820
+ * now paid on the interval instead — see `occupancyRecheckSecField`.
17821
+ */
17822
+ occupancyRecheckEnabled: boolean().default(true),
17489
17823
  occupancyRecheckSec: number().min(occupancyRecheckSecField.min).max(occupancyRecheckSecField.max).default(occupancyRecheckSecField.default),
17490
17824
  occupancyRecheckFrames: number().min(occupancyRecheckFramesField.min).max(occupancyRecheckFramesField.max).default(occupancyRecheckFramesField.default),
17491
17825
  /**
@@ -23588,7 +23922,7 @@ method(object({
23588
23922
  toMs: number()
23589
23923
  }), RecordingAvailabilitySchema, {
23590
23924
  kind: "query",
23591
- auth: "admin"
23925
+ auth: "protected"
23592
23926
  }), method(object({
23593
23927
  deviceId: number(),
23594
23928
  fromMs: number(),
@@ -23596,14 +23930,14 @@ method(object({
23596
23930
  tzOffsetMinutes: number()
23597
23931
  }), RecordingDaysSchema, {
23598
23932
  kind: "query",
23599
- auth: "admin"
23933
+ auth: "protected"
23600
23934
  }), method(object({
23601
23935
  deviceId: number(),
23602
23936
  fromMs: number(),
23603
23937
  toMs: number()
23604
23938
  }), RecordingManifestSchema, {
23605
23939
  kind: "query",
23606
- auth: "admin"
23940
+ auth: "protected"
23607
23941
  }), method(object({}), RecordingStorageUsageSchema, {
23608
23942
  kind: "query",
23609
23943
  auth: "admin"
@@ -23898,9 +24232,33 @@ method(object({
23898
24232
  * settings-contribution methods. `status.kind:'push'` — the engine pushes on
23899
24233
  * every hysteresis flip / availability change; consumers never poll.
23900
24234
  */
23901
- /** Extensible condition tag. Seeded 'day' | 'night'; open by design so more can
23902
- * be added without a wire break (matching falls back to any-condition refs). */
24235
+ /** Extensible condition tag. Seeded 'day' | 'ir' (the two variants the operator
24236
+ * captures) plus 'night' | 'dawn' | 'dusk' from the resolver's sun-times band.
24237
+ * Open by design so more can be added without a wire break.
24238
+ *
24239
+ * Matching does NOT fall back across conditions: cross-condition cosines are
24240
+ * not comparable, so "I have never seen this scene in this light" is reported
24241
+ * as `unknown`, never guessed. A day reference scored against an IR frame
24242
+ * collapses the cosine and would latch a false alarm every single night. */
23903
24243
  var SceneConditionSchema = string();
24244
+ /** `matched` = the baseline is what we see; `diverged` = it demonstrably is not;
24245
+ * `unknown` = we cannot judge (no reference for this condition, encoder model
24246
+ * changed, view shifted, no snapshot). `unknown` is a real value, not a null,
24247
+ * and never counts toward hysteresis in either direction. */
24248
+ var SceneVerdictSchema = _enum([
24249
+ "matched",
24250
+ "diverged",
24251
+ "unknown"
24252
+ ]);
24253
+ /** Why a scene cannot judge. Named, because this feature's failure mode is
24254
+ * silence that reads as "nothing has happened". */
24255
+ var SceneUnavailableSchema = _enum([
24256
+ "no-reference-for-condition",
24257
+ "view-shifted",
24258
+ "no-vision-profile",
24259
+ "encoder-model-changed",
24260
+ "no-snapshot"
24261
+ ]);
23904
24262
  /** One captured reference — condition-tagged, model-version-gated. `embedding`
23905
24263
  * is `number[]` (Float32Array does NOT survive MsgPack/UDS). */
23906
24264
  var SceneReferenceSchema = object({
@@ -23908,7 +24266,14 @@ var SceneReferenceSchema = object({
23908
24266
  modelId: string(),
23909
24267
  condition: SceneConditionSchema,
23910
24268
  capturedAt: number(),
23911
- thumbnailMediaId: string().optional()
24269
+ thumbnailMediaId: string().optional(),
24270
+ /** Whole-frame (downscaled) embedding captured alongside the ROI crop. The
24271
+ * anti-view-shift anchor: a bumped camera, a PTZ preset or a re-aim makes the
24272
+ * normalized rect frame a different piece of world, and the scene would
24273
+ * diverge forever with a perfectly plausible cosine. Checked LAZILY, only
24274
+ * when hysteresis is about to flip — one extra encode per candidate
24275
+ * transition, not per poll. */
24276
+ anchorEmbedding: array(number()).optional()
23912
24277
  });
23913
24278
  var SceneMonitorStateSchema = object({
23914
24279
  id: string(),
@@ -23930,6 +24295,25 @@ var SceneCheckSchema = discriminatedUnion("mode", [object({
23930
24295
  profileId: string().optional(),
23931
24296
  hysteresisCount: number().int().positive()
23932
24297
  })]);
24298
+ var SCENE_DEFAULT_ANCHOR_THRESHOLD = .85;
24299
+ /**
24300
+ * Vision-model adjudication of a candidate flip. Field names deliberately
24301
+ * mirror `NcConfirmSchema` so an operator meets one vocabulary, not two.
24302
+ *
24303
+ * `onTimeout` defaults to **'hold'**, the OPPOSITE of `NcConfirmGate`'s
24304
+ * fail-open: a notification suppressed is the worse error there, but a vision
24305
+ * model that timed out has not told us the bin is gone, and a latch is a
24306
+ * stateful claim that costs the operator a trip to reset.
24307
+ */
24308
+ var SceneConfirmSchema = object({
24309
+ enabled: boolean().default(false),
24310
+ prompt: string().min(1).max(1e3),
24311
+ profileId: string().optional(),
24312
+ timeoutMs: number().int().min(1e3).max(2e4).default(8e3),
24313
+ maxImagePx: number().int().min(64).max(2048).default(448),
24314
+ /** What a timeout / unavailable model means for the PENDING flip. */
24315
+ onTimeout: _enum(["flip", "hold"]).default("hold")
24316
+ });
23933
24317
  var SceneMonitorSchema = object({
23934
24318
  id: string(),
23935
24319
  label: string(),
@@ -23948,7 +24332,41 @@ var SceneMonitorSchema = object({
23948
24332
  lastConfidence: number().nullable(),
23949
24333
  currentCondition: SceneConditionSchema.nullable(),
23950
24334
  availability: _enum(["ok", "unavailable"]),
23951
- unavailableReason: string().nullable()
24335
+ unavailableReason: string().nullable(),
24336
+ /** Which state is "the initial screen". `null` until the first capture. */
24337
+ baselineStateId: string().nullable(),
24338
+ /** Which boolean drives notification rules and any export. */
24339
+ emit: _enum(["latched", "live"]).default("latched"),
24340
+ /** Live: does the region match the baseline RIGHT NOW. */
24341
+ verdict: SceneVerdictSchema,
24342
+ /** Has it been `diverged` at least once since `armedAt` — the operator's boolean. */
24343
+ latched: boolean(),
24344
+ /** Last reset (or creation). */
24345
+ armedAt: number(),
24346
+ divergedAt: number().nullable(),
24347
+ restoredAt: number().nullable(),
24348
+ /** A check is only COUNTED when the device has been quiet this long. Motion
24349
+ * during the window DISCARDS the observation — a car pulling up in front of
24350
+ * the bin must not be able to spend hysteresis credit. */
24351
+ quietSeconds: number().int().min(0).max(3600).default(60),
24352
+ /** An observation only advances the pending count when it is at least this
24353
+ * far from the previously counted one, so N agreeing checks span real time
24354
+ * rather than N adjacent polls inside one occlusion. */
24355
+ minObservationSpacingSec: number().int().min(0).max(3600).default(120),
24356
+ /** Vision-model adjudication of a candidate flip. Similarity primary only. */
24357
+ confirm: SceneConfirmSchema.optional(),
24358
+ /** Whole-frame anchor cosine below which a flip is REFUSED as `view-shifted`. */
24359
+ anchorThreshold: number().min(0).max(1).default(SCENE_DEFAULT_ANCHOR_THRESHOLD),
24360
+ /** Clear the latch on its own when the scene matches again? Default false —
24361
+ * `restoredAt` and the `scene-restored` edge are recorded regardless, so an
24362
+ * automation can react to the bin coming back without the operator's own
24363
+ * alarm silently clearing itself. */
24364
+ autoRestore: boolean().default(false),
24365
+ /** Named cause when `verdict === 'unknown'`. */
24366
+ unavailable: SceneUnavailableSchema.nullable(),
24367
+ /** Conditions that have at least one comparable reference — the coverage line
24368
+ * ("day ✓ · ir ✓ · dusk ✗") that turns a silent fallback into a visible fact. */
24369
+ coveredConditions: array(SceneConditionSchema)
23952
24370
  });
23953
24371
  var SceneMonitorStatusSchema = object({
23954
24372
  monitors: array(SceneMonitorSchema),
@@ -23981,7 +24399,14 @@ DeviceType.Camera, method(object({ deviceId: number() }), SceneMonitorStatusSche
23981
24399
  "both"
23982
24400
  ]).optional(),
23983
24401
  checkIntervalSec: number().optional(),
23984
- check: SceneCheckSchema.optional()
24402
+ check: SceneCheckSchema.optional(),
24403
+ emit: _enum(["latched", "live"]).optional(),
24404
+ quietSeconds: number().int().min(0).max(3600).optional(),
24405
+ minObservationSpacingSec: number().int().min(0).max(3600).optional(),
24406
+ anchorThreshold: number().min(0).max(1).optional(),
24407
+ autoRestore: boolean().optional(),
24408
+ /** `null` clears the vision-model adjudicator. */
24409
+ confirm: SceneConfirmSchema.nullable().optional()
23985
24410
  })
23986
24411
  }), _void(), {
23987
24412
  kind: "mutation",
@@ -24018,6 +24443,14 @@ DeviceType.Camera, method(object({ deviceId: number() }), SceneMonitorStatusSche
24018
24443
  }), _void(), {
24019
24444
  kind: "mutation",
24020
24445
  auth: "admin"
24446
+ }), method(object({
24447
+ deviceId: number(),
24448
+ monitorId: string(),
24449
+ /** Defaults to TRUE at the provider seam — see `SCENE_RESET_RECAPTURES`. */
24450
+ recapture: boolean().optional()
24451
+ }), _void(), {
24452
+ kind: "mutation",
24453
+ auth: "admin"
24021
24454
  });
24022
24455
  /**
24023
24456
  * Per-stage gating mode applied to the zones a rule references.
@@ -29311,6 +29744,12 @@ Object.freeze({
29311
29744
  addonId: null,
29312
29745
  access: "create"
29313
29746
  },
29747
+ "sceneMonitor.resetScene": {
29748
+ capName: "scene-monitor",
29749
+ capScope: "device",
29750
+ addonId: null,
29751
+ access: "delete"
29752
+ },
29314
29753
  "sceneMonitor.updateScene": {
29315
29754
  capName: "scene-monitor",
29316
29755
  capScope: "device",
@@ -30602,6 +31041,1683 @@ Object.freeze({
30602
31041
  access: "create"
30603
31042
  }
30604
31043
  });
31044
+ Object.freeze({
31045
+ "accessories.setChildHidden": [{
31046
+ name: "childDeviceId",
31047
+ form: "single",
31048
+ optional: false
31049
+ }, {
31050
+ name: "deviceId",
31051
+ form: "single",
31052
+ optional: false
31053
+ }],
31054
+ "addonSettings.getDeviceSettings": [{
31055
+ name: "deviceId",
31056
+ form: "single",
31057
+ optional: false
31058
+ }],
31059
+ "addonSettings.updateDeviceSettings": [{
31060
+ name: "deviceId",
31061
+ form: "single",
31062
+ optional: false
31063
+ }],
31064
+ "alarmPanel.arm": [{
31065
+ name: "deviceId",
31066
+ form: "single",
31067
+ optional: false
31068
+ }],
31069
+ "alarmPanel.disarm": [{
31070
+ name: "deviceId",
31071
+ form: "single",
31072
+ optional: false
31073
+ }],
31074
+ "alarmPanel.trigger": [{
31075
+ name: "deviceId",
31076
+ form: "single",
31077
+ optional: false
31078
+ }],
31079
+ "audioAnalysis.resolveDeviceSettings": [{
31080
+ name: "deviceId",
31081
+ form: "single",
31082
+ optional: false
31083
+ }],
31084
+ "audioAnalyzer.classify": [{
31085
+ name: "deviceId",
31086
+ form: "single",
31087
+ optional: true
31088
+ }],
31089
+ "audioMetrics.getCurrentSnapshot": [{
31090
+ name: "deviceId",
31091
+ form: "single",
31092
+ optional: false
31093
+ }],
31094
+ "audioMetrics.getHistory": [{
31095
+ name: "deviceId",
31096
+ form: "single",
31097
+ optional: false
31098
+ }],
31099
+ "automationControl.disable": [{
31100
+ name: "deviceId",
31101
+ form: "single",
31102
+ optional: false
31103
+ }],
31104
+ "automationControl.enable": [{
31105
+ name: "deviceId",
31106
+ form: "single",
31107
+ optional: false
31108
+ }],
31109
+ "automationControl.trigger": [{
31110
+ name: "deviceId",
31111
+ form: "single",
31112
+ optional: false
31113
+ }],
31114
+ "battery.wakeForStream": [{
31115
+ name: "deviceId",
31116
+ form: "single",
31117
+ optional: false
31118
+ }],
31119
+ "brightness.setBrightness": [{
31120
+ name: "deviceId",
31121
+ form: "single",
31122
+ optional: false
31123
+ }],
31124
+ "button.press": [{
31125
+ name: "deviceId",
31126
+ form: "single",
31127
+ optional: false
31128
+ }],
31129
+ "cameraCredentials.getCredentials": [{
31130
+ name: "deviceId",
31131
+ form: "single",
31132
+ optional: false
31133
+ }],
31134
+ "cameraStreams.getBrokerStreams": [{
31135
+ name: "deviceId",
31136
+ form: "single",
31137
+ optional: false
31138
+ }],
31139
+ "cameraStreams.getCameraStreams": [{
31140
+ name: "deviceId",
31141
+ form: "single",
31142
+ optional: false
31143
+ }],
31144
+ "cameraStreams.getProfileRtspEntries": [{
31145
+ name: "deviceId",
31146
+ form: "single",
31147
+ optional: false
31148
+ }],
31149
+ "cameraStreams.getRtspEntries": [{
31150
+ name: "deviceId",
31151
+ form: "single",
31152
+ optional: false
31153
+ }],
31154
+ "cameraStreams.pickStream": [{
31155
+ name: "deviceId",
31156
+ form: "single",
31157
+ optional: false
31158
+ }],
31159
+ "climateControl.setFanMode": [{
31160
+ name: "deviceId",
31161
+ form: "single",
31162
+ optional: false
31163
+ }],
31164
+ "climateControl.setMode": [{
31165
+ name: "deviceId",
31166
+ form: "single",
31167
+ optional: false
31168
+ }],
31169
+ "climateControl.setPreset": [{
31170
+ name: "deviceId",
31171
+ form: "single",
31172
+ optional: false
31173
+ }],
31174
+ "climateControl.setSwingHorizontal": [{
31175
+ name: "deviceId",
31176
+ form: "single",
31177
+ optional: false
31178
+ }],
31179
+ "climateControl.setSwingVertical": [{
31180
+ name: "deviceId",
31181
+ form: "single",
31182
+ optional: false
31183
+ }],
31184
+ "climateControl.setTarget": [{
31185
+ name: "deviceId",
31186
+ form: "single",
31187
+ optional: false
31188
+ }],
31189
+ "climateControl.setTargetHumidity": [{
31190
+ name: "deviceId",
31191
+ form: "single",
31192
+ optional: false
31193
+ }],
31194
+ "climateControl.setTargetRange": [{
31195
+ name: "deviceId",
31196
+ form: "single",
31197
+ optional: false
31198
+ }],
31199
+ "color.setColor": [{
31200
+ name: "deviceId",
31201
+ form: "single",
31202
+ optional: false
31203
+ }],
31204
+ "consumables.reset": [{
31205
+ name: "deviceId",
31206
+ form: "single",
31207
+ optional: false
31208
+ }],
31209
+ "control.setValue": [{
31210
+ name: "deviceId",
31211
+ form: "single",
31212
+ optional: false
31213
+ }],
31214
+ "cover.close": [{
31215
+ name: "deviceId",
31216
+ form: "single",
31217
+ optional: false
31218
+ }],
31219
+ "cover.open": [{
31220
+ name: "deviceId",
31221
+ form: "single",
31222
+ optional: false
31223
+ }],
31224
+ "cover.setPosition": [{
31225
+ name: "deviceId",
31226
+ form: "single",
31227
+ optional: false
31228
+ }],
31229
+ "cover.setTiltPosition": [{
31230
+ name: "deviceId",
31231
+ form: "single",
31232
+ optional: false
31233
+ }],
31234
+ "cover.stop": [{
31235
+ name: "deviceId",
31236
+ form: "single",
31237
+ optional: false
31238
+ }],
31239
+ "dayNight.getOptions": [{
31240
+ name: "deviceId",
31241
+ form: "single",
31242
+ optional: false
31243
+ }],
31244
+ "dayNight.setSettings": [{
31245
+ name: "deviceId",
31246
+ form: "single",
31247
+ optional: false
31248
+ }],
31249
+ "decoder.createSession": [{
31250
+ name: "deviceId",
31251
+ form: "single",
31252
+ optional: true
31253
+ }],
31254
+ "deviceAdoption.release": [{
31255
+ name: "camDeviceId",
31256
+ form: "single",
31257
+ optional: false
31258
+ }],
31259
+ "deviceAdoption.resync": [{
31260
+ name: "camDeviceId",
31261
+ form: "single",
31262
+ optional: false
31263
+ }],
31264
+ "deviceDiscovery.adoptDevice": [{
31265
+ name: "deviceId",
31266
+ form: "single",
31267
+ optional: false
31268
+ }],
31269
+ "deviceDiscovery.listDiscovered": [{
31270
+ name: "deviceId",
31271
+ form: "single",
31272
+ optional: false
31273
+ }],
31274
+ "deviceDiscovery.refreshDiscovery": [{
31275
+ name: "deviceId",
31276
+ form: "single",
31277
+ optional: false
31278
+ }],
31279
+ "deviceDiscovery.releaseDevice": [{
31280
+ name: "childDeviceId",
31281
+ form: "single",
31282
+ optional: false
31283
+ }, {
31284
+ name: "deviceId",
31285
+ form: "single",
31286
+ optional: false
31287
+ }],
31288
+ "deviceManager.adoptionRelease": [{
31289
+ name: "camDeviceId",
31290
+ form: "single",
31291
+ optional: false
31292
+ }],
31293
+ "deviceManager.adoptionResync": [{
31294
+ name: "camDeviceId",
31295
+ form: "single",
31296
+ optional: false
31297
+ }],
31298
+ "deviceManager.applyInitialMeta": [{
31299
+ name: "deviceId",
31300
+ form: "single",
31301
+ optional: false
31302
+ }, {
31303
+ name: "linkDeviceId",
31304
+ form: "single",
31305
+ optional: true
31306
+ }],
31307
+ "deviceManager.disable": [{
31308
+ name: "deviceId",
31309
+ form: "single",
31310
+ optional: false
31311
+ }],
31312
+ "deviceManager.enable": [{
31313
+ name: "deviceId",
31314
+ form: "single",
31315
+ optional: false
31316
+ }],
31317
+ "deviceManager.getBindings": [{
31318
+ name: "deviceId",
31319
+ form: "single",
31320
+ optional: false
31321
+ }],
31322
+ "deviceManager.getChildren": [{
31323
+ name: "parentDeviceId",
31324
+ form: "single",
31325
+ optional: false
31326
+ }],
31327
+ "deviceManager.getConfigSchema": [{
31328
+ name: "deviceId",
31329
+ form: "single",
31330
+ optional: false
31331
+ }],
31332
+ "deviceManager.getDevice": [{
31333
+ name: "deviceId",
31334
+ form: "single",
31335
+ optional: false
31336
+ }],
31337
+ "deviceManager.getDeviceAggregate": [{
31338
+ name: "deviceId",
31339
+ form: "single",
31340
+ optional: false
31341
+ }],
31342
+ "deviceManager.getDeviceLiveInfoAggregate": [{
31343
+ name: "deviceId",
31344
+ form: "single",
31345
+ optional: false
31346
+ }],
31347
+ "deviceManager.getDeviceSettingsAggregate": [{
31348
+ name: "deviceId",
31349
+ form: "single",
31350
+ optional: false
31351
+ }],
31352
+ "deviceManager.getDeviceStatusAggregate": [{
31353
+ name: "deviceId",
31354
+ form: "single",
31355
+ optional: false
31356
+ }],
31357
+ "deviceManager.getDeviceStatusAggregateBatch": [{
31358
+ name: "deviceIds",
31359
+ form: "array",
31360
+ optional: false
31361
+ }],
31362
+ "deviceManager.getLinkedDevices": [{
31363
+ name: "deviceId",
31364
+ form: "single",
31365
+ optional: false
31366
+ }],
31367
+ "deviceManager.getSettingsSchema": [{
31368
+ name: "deviceId",
31369
+ form: "single",
31370
+ optional: false
31371
+ }],
31372
+ "deviceManager.getStreamProfileMap": [{
31373
+ name: "deviceId",
31374
+ form: "single",
31375
+ optional: false
31376
+ }],
31377
+ "deviceManager.getStreamSources": [{
31378
+ name: "deviceId",
31379
+ form: "single",
31380
+ optional: false
31381
+ }],
31382
+ "deviceManager.getWireableFields": [{
31383
+ name: "deviceId",
31384
+ form: "single",
31385
+ optional: false
31386
+ }],
31387
+ "deviceManager.loadConfig": [{
31388
+ name: "deviceId",
31389
+ form: "single",
31390
+ optional: false
31391
+ }],
31392
+ "deviceManager.loadMeta": [{
31393
+ name: "deviceId",
31394
+ form: "single",
31395
+ optional: false
31396
+ }],
31397
+ "deviceManager.loadRuntimeState": [{
31398
+ name: "deviceId",
31399
+ form: "single",
31400
+ optional: false
31401
+ }],
31402
+ "deviceManager.persistConfig": [{
31403
+ name: "deviceId",
31404
+ form: "single",
31405
+ optional: false
31406
+ }],
31407
+ "deviceManager.probeStreams": [{
31408
+ name: "deviceId",
31409
+ form: "single",
31410
+ optional: false
31411
+ }],
31412
+ "deviceManager.registerDevice": [{
31413
+ name: "parentDeviceId",
31414
+ form: "single",
31415
+ optional: true
31416
+ }],
31417
+ "deviceManager.remove": [{
31418
+ name: "deviceId",
31419
+ form: "single",
31420
+ optional: false
31421
+ }],
31422
+ "deviceManager.removeDevice": [{
31423
+ name: "deviceId",
31424
+ form: "single",
31425
+ optional: false
31426
+ }],
31427
+ "deviceManager.runDeviceAction": [{
31428
+ name: "deviceId",
31429
+ form: "single",
31430
+ optional: false
31431
+ }],
31432
+ "deviceManager.setChildLayout": [{
31433
+ name: "deviceId",
31434
+ form: "single",
31435
+ optional: false
31436
+ }],
31437
+ "deviceManager.setDisabled": [{
31438
+ name: "deviceId",
31439
+ form: "single",
31440
+ optional: false
31441
+ }],
31442
+ "deviceManager.setDisplay": [{
31443
+ name: "deviceId",
31444
+ form: "single",
31445
+ optional: false
31446
+ }],
31447
+ "deviceManager.setIntegrationId": [{
31448
+ name: "deviceId",
31449
+ form: "single",
31450
+ optional: false
31451
+ }],
31452
+ "deviceManager.setLinkDeviceId": [{
31453
+ name: "deviceId",
31454
+ form: "single",
31455
+ optional: false
31456
+ }, {
31457
+ name: "linkDeviceId",
31458
+ form: "single",
31459
+ optional: true
31460
+ }],
31461
+ "deviceManager.setLocation": [{
31462
+ name: "deviceId",
31463
+ form: "single",
31464
+ optional: false
31465
+ }],
31466
+ "deviceManager.setMetadata": [{
31467
+ name: "deviceId",
31468
+ form: "single",
31469
+ optional: false
31470
+ }],
31471
+ "deviceManager.setName": [{
31472
+ name: "deviceId",
31473
+ form: "single",
31474
+ optional: false
31475
+ }],
31476
+ "deviceManager.setPrimaryChildEntityId": [{
31477
+ name: "deviceId",
31478
+ form: "single",
31479
+ optional: false
31480
+ }],
31481
+ "deviceManager.setRole": [{
31482
+ name: "deviceId",
31483
+ form: "single",
31484
+ optional: false
31485
+ }],
31486
+ "deviceManager.setStreamProfileMap": [{
31487
+ name: "deviceId",
31488
+ form: "single",
31489
+ optional: false
31490
+ }],
31491
+ "deviceManager.setType": [{
31492
+ name: "deviceId",
31493
+ form: "single",
31494
+ optional: false
31495
+ }],
31496
+ "deviceManager.setWrapperActive": [{
31497
+ name: "deviceId",
31498
+ form: "single",
31499
+ optional: false
31500
+ }],
31501
+ "deviceManager.testField": [{
31502
+ name: "deviceId",
31503
+ form: "single",
31504
+ optional: false
31505
+ }],
31506
+ "deviceManager.updateConfig": [{
31507
+ name: "deviceId",
31508
+ form: "single",
31509
+ optional: false
31510
+ }],
31511
+ "deviceManager.updateDeviceField": [{
31512
+ name: "deviceId",
31513
+ form: "single",
31514
+ optional: false
31515
+ }],
31516
+ "deviceManager.updateDeviceFieldsBatch": [{
31517
+ name: "deviceId",
31518
+ form: "single",
31519
+ optional: false
31520
+ }],
31521
+ "deviceOps.getConfigEntries": [{
31522
+ name: "deviceId",
31523
+ form: "single",
31524
+ optional: false
31525
+ }],
31526
+ "deviceOps.getRawState": [{
31527
+ name: "deviceId",
31528
+ form: "single",
31529
+ optional: false
31530
+ }],
31531
+ "deviceOps.getSettingsSchema": [{
31532
+ name: "deviceId",
31533
+ form: "single",
31534
+ optional: false
31535
+ }],
31536
+ "deviceOps.getStreamSources": [{
31537
+ name: "deviceId",
31538
+ form: "single",
31539
+ optional: false
31540
+ }],
31541
+ "deviceOps.removeDevice": [{
31542
+ name: "deviceId",
31543
+ form: "single",
31544
+ optional: false
31545
+ }],
31546
+ "deviceOps.runAction": [{
31547
+ name: "deviceId",
31548
+ form: "single",
31549
+ optional: false
31550
+ }],
31551
+ "deviceOps.setConfig": [{
31552
+ name: "deviceId",
31553
+ form: "single",
31554
+ optional: false
31555
+ }],
31556
+ "deviceState.getCapSlice": [{
31557
+ name: "deviceId",
31558
+ form: "single",
31559
+ optional: false
31560
+ }],
31561
+ "deviceState.getSnapshot": [{
31562
+ name: "deviceId",
31563
+ form: "single",
31564
+ optional: false
31565
+ }],
31566
+ "deviceState.setCapSlice": [{
31567
+ name: "deviceId",
31568
+ form: "single",
31569
+ optional: false
31570
+ }],
31571
+ "events.getEventClipUrl": [{
31572
+ name: "deviceId",
31573
+ form: "single",
31574
+ optional: false
31575
+ }],
31576
+ "events.getEvents": [{
31577
+ name: "deviceId",
31578
+ form: "single",
31579
+ optional: false
31580
+ }],
31581
+ "events.getEventThumbnail": [{
31582
+ name: "deviceId",
31583
+ form: "single",
31584
+ optional: false
31585
+ }],
31586
+ "faceGallery.getFaceByTrack": [{
31587
+ name: "deviceId",
31588
+ form: "single",
31589
+ optional: false
31590
+ }],
31591
+ "faceGallery.listRecentFaces": [{
31592
+ name: "deviceId",
31593
+ form: "single",
31594
+ optional: true
31595
+ }],
31596
+ "fanControl.setDirection": [{
31597
+ name: "deviceId",
31598
+ form: "single",
31599
+ optional: false
31600
+ }],
31601
+ "fanControl.setOscillating": [{
31602
+ name: "deviceId",
31603
+ form: "single",
31604
+ optional: false
31605
+ }],
31606
+ "fanControl.setPercentage": [{
31607
+ name: "deviceId",
31608
+ form: "single",
31609
+ optional: false
31610
+ }],
31611
+ "fanControl.setPreset": [{
31612
+ name: "deviceId",
31613
+ form: "single",
31614
+ optional: false
31615
+ }],
31616
+ "humidifier.setMode": [{
31617
+ name: "deviceId",
31618
+ form: "single",
31619
+ optional: false
31620
+ }],
31621
+ "humidifier.setOn": [{
31622
+ name: "deviceId",
31623
+ form: "single",
31624
+ optional: false
31625
+ }],
31626
+ "humidifier.setTargetHumidity": [{
31627
+ name: "deviceId",
31628
+ form: "single",
31629
+ optional: false
31630
+ }],
31631
+ "imageSettings.getOptions": [{
31632
+ name: "deviceId",
31633
+ form: "single",
31634
+ optional: false
31635
+ }],
31636
+ "imageSettings.setSettings": [{
31637
+ name: "deviceId",
31638
+ form: "single",
31639
+ optional: false
31640
+ }],
31641
+ "intercom.endTalkSession": [{
31642
+ name: "deviceId",
31643
+ form: "single",
31644
+ optional: false
31645
+ }],
31646
+ "intercom.handleAnswer": [{
31647
+ name: "deviceId",
31648
+ form: "single",
31649
+ optional: false
31650
+ }],
31651
+ "intercom.pushTalkAudio": [{
31652
+ name: "deviceId",
31653
+ form: "single",
31654
+ optional: false
31655
+ }],
31656
+ "intercom.startSession": [{
31657
+ name: "deviceId",
31658
+ form: "single",
31659
+ optional: false
31660
+ }],
31661
+ "intercom.startTalkSession": [{
31662
+ name: "deviceId",
31663
+ form: "single",
31664
+ optional: false
31665
+ }],
31666
+ "intercom.stopSession": [{
31667
+ name: "deviceId",
31668
+ form: "single",
31669
+ optional: false
31670
+ }],
31671
+ "lawnMowerControl.dock": [{
31672
+ name: "deviceId",
31673
+ form: "single",
31674
+ optional: false
31675
+ }],
31676
+ "lawnMowerControl.pause": [{
31677
+ name: "deviceId",
31678
+ form: "single",
31679
+ optional: false
31680
+ }],
31681
+ "lawnMowerControl.startMowing": [{
31682
+ name: "deviceId",
31683
+ form: "single",
31684
+ optional: false
31685
+ }],
31686
+ "lockControl.lock": [{
31687
+ name: "deviceId",
31688
+ form: "single",
31689
+ optional: false
31690
+ }],
31691
+ "lockControl.open": [{
31692
+ name: "deviceId",
31693
+ form: "single",
31694
+ optional: false
31695
+ }],
31696
+ "lockControl.unlock": [{
31697
+ name: "deviceId",
31698
+ form: "single",
31699
+ optional: false
31700
+ }],
31701
+ "mediaPlayer.next": [{
31702
+ name: "deviceId",
31703
+ form: "single",
31704
+ optional: false
31705
+ }],
31706
+ "mediaPlayer.pause": [{
31707
+ name: "deviceId",
31708
+ form: "single",
31709
+ optional: false
31710
+ }],
31711
+ "mediaPlayer.play": [{
31712
+ name: "deviceId",
31713
+ form: "single",
31714
+ optional: false
31715
+ }],
31716
+ "mediaPlayer.playMedia": [{
31717
+ name: "deviceId",
31718
+ form: "single",
31719
+ optional: false
31720
+ }],
31721
+ "mediaPlayer.previous": [{
31722
+ name: "deviceId",
31723
+ form: "single",
31724
+ optional: false
31725
+ }],
31726
+ "mediaPlayer.seek": [{
31727
+ name: "deviceId",
31728
+ form: "single",
31729
+ optional: false
31730
+ }],
31731
+ "mediaPlayer.selectSource": [{
31732
+ name: "deviceId",
31733
+ form: "single",
31734
+ optional: false
31735
+ }],
31736
+ "mediaPlayer.setMute": [{
31737
+ name: "deviceId",
31738
+ form: "single",
31739
+ optional: false
31740
+ }],
31741
+ "mediaPlayer.setRepeat": [{
31742
+ name: "deviceId",
31743
+ form: "single",
31744
+ optional: false
31745
+ }],
31746
+ "mediaPlayer.setShuffle": [{
31747
+ name: "deviceId",
31748
+ form: "single",
31749
+ optional: false
31750
+ }],
31751
+ "mediaPlayer.setVolume": [{
31752
+ name: "deviceId",
31753
+ form: "single",
31754
+ optional: false
31755
+ }],
31756
+ "mediaPlayer.stop": [{
31757
+ name: "deviceId",
31758
+ form: "single",
31759
+ optional: false
31760
+ }],
31761
+ "motion.isDetected": [{
31762
+ name: "deviceId",
31763
+ form: "single",
31764
+ optional: false
31765
+ }],
31766
+ "motionDetection.analyze": [{
31767
+ name: "deviceId",
31768
+ form: "single",
31769
+ optional: false
31770
+ }],
31771
+ "motionDetection.removeCamera": [{
31772
+ name: "deviceId",
31773
+ form: "single",
31774
+ optional: false
31775
+ }],
31776
+ "motionTrigger.setMotionTrigger": [{
31777
+ name: "deviceId",
31778
+ form: "single",
31779
+ optional: false
31780
+ }],
31781
+ "motionZones.getOptions": [{
31782
+ name: "deviceId",
31783
+ form: "single",
31784
+ optional: false
31785
+ }],
31786
+ "motionZones.setZone": [{
31787
+ name: "deviceId",
31788
+ form: "single",
31789
+ optional: false
31790
+ }],
31791
+ "nativeObjectDetection.setEnabled": [{
31792
+ name: "deviceId",
31793
+ form: "single",
31794
+ optional: false
31795
+ }],
31796
+ "networkQuality.getDeviceStats": [{
31797
+ name: "deviceId",
31798
+ form: "single",
31799
+ optional: false
31800
+ }],
31801
+ "networkQuality.reportClientStats": [{
31802
+ name: "deviceId",
31803
+ form: "single",
31804
+ optional: false
31805
+ }],
31806
+ "notificationRules.setDeviceMuted": [{
31807
+ name: "deviceId",
31808
+ form: "single",
31809
+ optional: false
31810
+ }],
31811
+ "notifier.cancel": [{
31812
+ name: "deviceId",
31813
+ form: "single",
31814
+ optional: false
31815
+ }],
31816
+ "notifier.send": [{
31817
+ name: "deviceId",
31818
+ form: "single",
31819
+ optional: false
31820
+ }],
31821
+ "osd.setOverlay": [{
31822
+ name: "deviceId",
31823
+ form: "single",
31824
+ optional: false
31825
+ }],
31826
+ "osdManager.clearSlotBinding": [{
31827
+ name: "deviceId",
31828
+ form: "single",
31829
+ optional: false
31830
+ }],
31831
+ "osdManager.copyDeviceConfiguration": [{
31832
+ name: "sourceDeviceId",
31833
+ form: "single",
31834
+ optional: false
31835
+ }, {
31836
+ name: "targetDeviceId",
31837
+ form: "single",
31838
+ optional: false
31839
+ }],
31840
+ "osdManager.getDeviceOsd": [{
31841
+ name: "deviceId",
31842
+ form: "single",
31843
+ optional: false
31844
+ }],
31845
+ "osdManager.getSourceCatalog": [{
31846
+ name: "deviceId",
31847
+ form: "single",
31848
+ optional: false
31849
+ }],
31850
+ "osdManager.previewSlot": [{
31851
+ name: "deviceId",
31852
+ form: "single",
31853
+ optional: false
31854
+ }],
31855
+ "osdManager.renderDevice": [{
31856
+ name: "deviceId",
31857
+ form: "single",
31858
+ optional: false
31859
+ }],
31860
+ "osdManager.setSlotBinding": [{
31861
+ name: "deviceId",
31862
+ form: "single",
31863
+ optional: false
31864
+ }],
31865
+ "petFeeder.callPet": [{
31866
+ name: "deviceId",
31867
+ form: "single",
31868
+ optional: false
31869
+ }],
31870
+ "petFeeder.cancelFeed": [{
31871
+ name: "deviceId",
31872
+ form: "single",
31873
+ optional: false
31874
+ }],
31875
+ "petFeeder.feed": [{
31876
+ name: "deviceId",
31877
+ form: "single",
31878
+ optional: false
31879
+ }],
31880
+ "petFeeder.markFoodReplenished": [{
31881
+ name: "deviceId",
31882
+ form: "single",
31883
+ optional: false
31884
+ }],
31885
+ "petFeeder.playSound": [{
31886
+ name: "deviceId",
31887
+ form: "single",
31888
+ optional: false
31889
+ }],
31890
+ "petFeeder.resetDesiccant": [{
31891
+ name: "deviceId",
31892
+ form: "single",
31893
+ optional: false
31894
+ }],
31895
+ "petFeeder.setChildLock": [{
31896
+ name: "deviceId",
31897
+ form: "single",
31898
+ optional: false
31899
+ }],
31900
+ "petFeeder.setFeedSound": [{
31901
+ name: "deviceId",
31902
+ form: "single",
31903
+ optional: false
31904
+ }],
31905
+ "petFeeder.setIndicatorLight": [{
31906
+ name: "deviceId",
31907
+ form: "single",
31908
+ optional: false
31909
+ }],
31910
+ "petFeeder.setVolume": [{
31911
+ name: "deviceId",
31912
+ form: "single",
31913
+ optional: false
31914
+ }],
31915
+ "pipelineAnalytics.clearTracks": [{
31916
+ name: "deviceId",
31917
+ form: "single",
31918
+ optional: false
31919
+ }],
31920
+ "pipelineAnalytics.completeRetrainTrack": [{
31921
+ name: "deviceId",
31922
+ form: "single",
31923
+ optional: false
31924
+ }],
31925
+ "pipelineAnalytics.deleteDeviceEvents": [{
31926
+ name: "deviceId",
31927
+ form: "single",
31928
+ optional: false
31929
+ }],
31930
+ "pipelineAnalytics.deleteTracks": [{
31931
+ name: "deviceId",
31932
+ form: "single",
31933
+ optional: false
31934
+ }],
31935
+ "pipelineAnalytics.deselectRetrainFrame": [{
31936
+ name: "deviceId",
31937
+ form: "single",
31938
+ optional: false
31939
+ }],
31940
+ "pipelineAnalytics.getActiveTracks": [{
31941
+ name: "deviceId",
31942
+ form: "single",
31943
+ optional: false
31944
+ }],
31945
+ "pipelineAnalytics.getAudioEvents": [{
31946
+ name: "deviceId",
31947
+ form: "single",
31948
+ optional: false
31949
+ }],
31950
+ "pipelineAnalytics.getEventDensity": [{
31951
+ name: "deviceId",
31952
+ form: "single",
31953
+ optional: false
31954
+ }],
31955
+ "pipelineAnalytics.getKeyEvents": [{
31956
+ name: "deviceId",
31957
+ form: "single",
31958
+ optional: false
31959
+ }],
31960
+ "pipelineAnalytics.getMotionEvents": [{
31961
+ name: "deviceId",
31962
+ form: "single",
31963
+ optional: false
31964
+ }],
31965
+ "pipelineAnalytics.getObjectEvents": [{
31966
+ name: "deviceId",
31967
+ form: "single",
31968
+ optional: false
31969
+ }],
31970
+ "pipelineAnalytics.getRetrainExportUrl": [{
31971
+ name: "deviceIds",
31972
+ form: "array",
31973
+ optional: true
31974
+ }],
31975
+ "pipelineAnalytics.getSensorEvents": [{
31976
+ name: "deviceId",
31977
+ form: "single",
31978
+ optional: false
31979
+ }],
31980
+ "pipelineAnalytics.getTrack": [{
31981
+ name: "deviceId",
31982
+ form: "single",
31983
+ optional: false
31984
+ }],
31985
+ "pipelineAnalytics.getTrainingExportSummary": [{
31986
+ name: "deviceIds",
31987
+ form: "array",
31988
+ optional: true
31989
+ }],
31990
+ "pipelineAnalytics.getTrainingExportUrl": [{
31991
+ name: "deviceIds",
31992
+ form: "array",
31993
+ optional: true
31994
+ }],
31995
+ "pipelineAnalytics.listEventKinds": [{
31996
+ name: "deviceId",
31997
+ form: "single",
31998
+ optional: false
31999
+ }],
32000
+ "pipelineAnalytics.listEventKindsBatch": [{
32001
+ name: "deviceIds",
32002
+ form: "array",
32003
+ optional: false
32004
+ }],
32005
+ "pipelineAnalytics.listOpsLog": [{
32006
+ name: "deviceId",
32007
+ form: "single",
32008
+ optional: true
32009
+ }],
32010
+ "pipelineAnalytics.listRecentTracks": [{
32011
+ name: "deviceIds",
32012
+ form: "array",
32013
+ optional: false
32014
+ }],
32015
+ "pipelineAnalytics.listRetrainStaging": [{
32016
+ name: "deviceIds",
32017
+ form: "array",
32018
+ optional: true
32019
+ }],
32020
+ "pipelineAnalytics.listTracks": [{
32021
+ name: "deviceId",
32022
+ form: "single",
32023
+ optional: false
32024
+ }],
32025
+ "pipelineAnalytics.proposeRetrainAnnotations": [{
32026
+ name: "deviceId",
32027
+ form: "single",
32028
+ optional: false
32029
+ }],
32030
+ "pipelineAnalytics.pruneEventsBefore": [{
32031
+ name: "deviceId",
32032
+ form: "single",
32033
+ optional: false
32034
+ }],
32035
+ "pipelineAnalytics.pruneTracksBefore": [{
32036
+ name: "deviceId",
32037
+ form: "single",
32038
+ optional: false
32039
+ }],
32040
+ "pipelineAnalytics.rebuildObjectEmbeddings": [{
32041
+ name: "deviceId",
32042
+ form: "single",
32043
+ optional: true
32044
+ }],
32045
+ "pipelineAnalytics.restageRetrainTrack": [{
32046
+ name: "deviceId",
32047
+ form: "single",
32048
+ optional: false
32049
+ }],
32050
+ "pipelineAnalytics.saveRetrainAnnotations": [{
32051
+ name: "deviceId",
32052
+ form: "single",
32053
+ optional: false
32054
+ }],
32055
+ "pipelineAnalytics.searchObjectEvents": [{
32056
+ name: "deviceId",
32057
+ form: "single",
32058
+ optional: true
32059
+ }],
32060
+ "pipelineAnalytics.selectRetrainFrames": [{
32061
+ name: "deviceId",
32062
+ form: "single",
32063
+ optional: false
32064
+ }],
32065
+ "pipelineAnalytics.setTrackFlags": [{
32066
+ name: "deviceId",
32067
+ form: "single",
32068
+ optional: false
32069
+ }],
32070
+ "pipelineAnalytics.wipeAllAnalytics": [{
32071
+ name: "deviceId",
32072
+ form: "single",
32073
+ optional: false
32074
+ }],
32075
+ "pipelineExecutor.runPipeline": [{
32076
+ name: "deviceId",
32077
+ form: "single",
32078
+ optional: true
32079
+ }],
32080
+ "pipelineExecutor.runPipelineBatch": [{
32081
+ name: "deviceId",
32082
+ form: "single",
32083
+ optional: true
32084
+ }],
32085
+ "pipelineOrchestrator.assignAudio": [{
32086
+ name: "deviceId",
32087
+ form: "single",
32088
+ optional: false
32089
+ }],
32090
+ "pipelineOrchestrator.assignPipeline": [{
32091
+ name: "deviceId",
32092
+ form: "single",
32093
+ optional: false
32094
+ }],
32095
+ "pipelineOrchestrator.getAudioAssignment": [{
32096
+ name: "deviceId",
32097
+ form: "single",
32098
+ optional: false
32099
+ }],
32100
+ "pipelineOrchestrator.getCameraMetrics": [{
32101
+ name: "deviceId",
32102
+ form: "single",
32103
+ optional: false
32104
+ }],
32105
+ "pipelineOrchestrator.getCameraSettings": [{
32106
+ name: "deviceId",
32107
+ form: "single",
32108
+ optional: false
32109
+ }],
32110
+ "pipelineOrchestrator.getCameraStatus": [{
32111
+ name: "deviceId",
32112
+ form: "single",
32113
+ optional: false
32114
+ }],
32115
+ "pipelineOrchestrator.getCameraStatuses": [{
32116
+ name: "deviceIds",
32117
+ form: "array",
32118
+ optional: true
32119
+ }],
32120
+ "pipelineOrchestrator.getCameraStepOverrides": [{
32121
+ name: "deviceId",
32122
+ form: "single",
32123
+ optional: false
32124
+ }],
32125
+ "pipelineOrchestrator.getCameraSwitches": [{
32126
+ name: "deviceId",
32127
+ form: "single",
32128
+ optional: false
32129
+ }],
32130
+ "pipelineOrchestrator.getPipelineAssignment": [{
32131
+ name: "deviceId",
32132
+ form: "single",
32133
+ optional: false
32134
+ }],
32135
+ "pipelineOrchestrator.getPipelineDevicePin": [{
32136
+ name: "deviceId",
32137
+ form: "single",
32138
+ optional: false
32139
+ }],
32140
+ "pipelineOrchestrator.resolvePipeline": [{
32141
+ name: "deviceId",
32142
+ form: "single",
32143
+ optional: false
32144
+ }],
32145
+ "pipelineOrchestrator.setCameraPipelineForAgent": [{
32146
+ name: "deviceId",
32147
+ form: "single",
32148
+ optional: false
32149
+ }],
32150
+ "pipelineOrchestrator.setCameraStepOverride": [{
32151
+ name: "deviceId",
32152
+ form: "single",
32153
+ optional: false
32154
+ }],
32155
+ "pipelineOrchestrator.setCameraStepToggle": [{
32156
+ name: "deviceId",
32157
+ form: "single",
32158
+ optional: false
32159
+ }],
32160
+ "pipelineOrchestrator.setCameraSwitch": [{
32161
+ name: "deviceId",
32162
+ form: "single",
32163
+ optional: false
32164
+ }],
32165
+ "pipelineOrchestrator.setPipelineDevicePin": [{
32166
+ name: "deviceId",
32167
+ form: "single",
32168
+ optional: false
32169
+ }],
32170
+ "pipelineOrchestrator.unassignAudio": [{
32171
+ name: "deviceId",
32172
+ form: "single",
32173
+ optional: false
32174
+ }],
32175
+ "pipelineOrchestrator.unassignPipeline": [{
32176
+ name: "deviceId",
32177
+ form: "single",
32178
+ optional: false
32179
+ }],
32180
+ "pipelineRunner.attachCamera": [{
32181
+ name: "deviceId",
32182
+ form: "single",
32183
+ optional: false
32184
+ }],
32185
+ "pipelineRunner.detachCamera": [{
32186
+ name: "deviceId",
32187
+ form: "single",
32188
+ optional: false
32189
+ }],
32190
+ "pipelineRunner.getCameraMetrics": [{
32191
+ name: "deviceId",
32192
+ form: "single",
32193
+ optional: false
32194
+ }],
32195
+ "pipelineRunner.reportMotion": [{
32196
+ name: "deviceId",
32197
+ form: "single",
32198
+ optional: false
32199
+ }],
32200
+ "pipelineRunner.runDetailSubtree": [{
32201
+ name: "deviceId",
32202
+ form: "single",
32203
+ optional: false
32204
+ }],
32205
+ "pipelineRunner.runStatelessStep": [{
32206
+ name: "sourceDeviceId",
32207
+ form: "single",
32208
+ optional: false
32209
+ }],
32210
+ "plateGallery.getPlateByTrack": [{
32211
+ name: "deviceId",
32212
+ form: "single",
32213
+ optional: false
32214
+ }],
32215
+ "plateGallery.listPlates": [{
32216
+ name: "deviceId",
32217
+ form: "single",
32218
+ optional: true
32219
+ }],
32220
+ "privacyMask.getOptions": [{
32221
+ name: "deviceId",
32222
+ form: "single",
32223
+ optional: false
32224
+ }],
32225
+ "privacyMask.setAudioEnabled": [{
32226
+ name: "deviceId",
32227
+ form: "single",
32228
+ optional: false
32229
+ }],
32230
+ "privacyMask.setMask": [{
32231
+ name: "deviceId",
32232
+ form: "single",
32233
+ optional: false
32234
+ }],
32235
+ "ptz.continuousMove": [{
32236
+ name: "deviceId",
32237
+ form: "single",
32238
+ optional: false
32239
+ }],
32240
+ "ptz.deletePreset": [{
32241
+ name: "deviceId",
32242
+ form: "single",
32243
+ optional: false
32244
+ }],
32245
+ "ptz.getOptions": [{
32246
+ name: "deviceId",
32247
+ form: "single",
32248
+ optional: false
32249
+ }],
32250
+ "ptz.getPosition": [{
32251
+ name: "deviceId",
32252
+ form: "single",
32253
+ optional: false
32254
+ }],
32255
+ "ptz.getPresets": [{
32256
+ name: "deviceId",
32257
+ form: "single",
32258
+ optional: false
32259
+ }],
32260
+ "ptz.goHome": [{
32261
+ name: "deviceId",
32262
+ form: "single",
32263
+ optional: false
32264
+ }],
32265
+ "ptz.goToPreset": [{
32266
+ name: "deviceId",
32267
+ form: "single",
32268
+ optional: false
32269
+ }],
32270
+ "ptz.move": [{
32271
+ name: "deviceId",
32272
+ form: "single",
32273
+ optional: false
32274
+ }],
32275
+ "ptz.savePreset": [{
32276
+ name: "deviceId",
32277
+ form: "single",
32278
+ optional: false
32279
+ }],
32280
+ "ptz.setAutofocus": [{
32281
+ name: "deviceId",
32282
+ form: "single",
32283
+ optional: false
32284
+ }],
32285
+ "ptz.stop": [{
32286
+ name: "deviceId",
32287
+ form: "single",
32288
+ optional: false
32289
+ }],
32290
+ "ptzAutotrack.getSettings": [{
32291
+ name: "deviceId",
32292
+ form: "single",
32293
+ optional: false
32294
+ }],
32295
+ "ptzAutotrack.getStatus": [{
32296
+ name: "deviceId",
32297
+ form: "single",
32298
+ optional: false
32299
+ }],
32300
+ "ptzAutotrack.setEnabled": [{
32301
+ name: "deviceId",
32302
+ form: "single",
32303
+ optional: false
32304
+ }],
32305
+ "ptzAutotrack.setSettings": [{
32306
+ name: "deviceId",
32307
+ form: "single",
32308
+ optional: false
32309
+ }],
32310
+ "reboot.reboot": [{
32311
+ name: "deviceId",
32312
+ form: "single",
32313
+ optional: false
32314
+ }],
32315
+ "recording.deleteFootprint": [{
32316
+ name: "deviceId",
32317
+ form: "single",
32318
+ optional: false
32319
+ }],
32320
+ "recording.getAvailability": [{
32321
+ name: "deviceId",
32322
+ form: "single",
32323
+ optional: false
32324
+ }],
32325
+ "recording.getDaysWithRecordings": [{
32326
+ name: "deviceId",
32327
+ form: "single",
32328
+ optional: false
32329
+ }],
32330
+ "recording.getDeviceConfig": [{
32331
+ name: "deviceId",
32332
+ form: "single",
32333
+ optional: false
32334
+ }],
32335
+ "recording.getPlaybackManifest": [{
32336
+ name: "deviceId",
32337
+ form: "single",
32338
+ optional: false
32339
+ }],
32340
+ "recording.listOpsLog": [{
32341
+ name: "deviceId",
32342
+ form: "single",
32343
+ optional: true
32344
+ }],
32345
+ "recording.locateSegment": [{
32346
+ name: "deviceId",
32347
+ form: "single",
32348
+ optional: false
32349
+ }],
32350
+ "recording.pruneFootage": [{
32351
+ name: "deviceId",
32352
+ form: "single",
32353
+ optional: false
32354
+ }],
32355
+ "recording.readGopBytes": [{
32356
+ name: "deviceId",
32357
+ form: "single",
32358
+ optional: false
32359
+ }],
32360
+ "recording.readSegmentBytes": [{
32361
+ name: "deviceId",
32362
+ form: "single",
32363
+ optional: false
32364
+ }],
32365
+ "recording.relocateFootage": [{
32366
+ name: "deviceId",
32367
+ form: "single",
32368
+ optional: true
32369
+ }],
32370
+ "recording.renderClip": [{
32371
+ name: "deviceId",
32372
+ form: "single",
32373
+ optional: false
32374
+ }],
32375
+ "recording.renderGif": [{
32376
+ name: "deviceId",
32377
+ form: "single",
32378
+ optional: false
32379
+ }],
32380
+ "recording.rescanStorage": [{
32381
+ name: "deviceId",
32382
+ form: "single",
32383
+ optional: false
32384
+ }],
32385
+ "recording.setDeviceConfig": [{
32386
+ name: "deviceId",
32387
+ form: "single",
32388
+ optional: false
32389
+ }],
32390
+ "recording.startStorageMigrationMove": [{
32391
+ name: "deviceId",
32392
+ form: "single",
32393
+ optional: true
32394
+ }],
32395
+ "recordingExport.createExport": [{
32396
+ name: "deviceId",
32397
+ form: "single",
32398
+ optional: false
32399
+ }],
32400
+ "recordingExport.listExports": [{
32401
+ name: "deviceId",
32402
+ form: "single",
32403
+ optional: true
32404
+ }],
32405
+ "sceneMonitor.captureReference": [{
32406
+ name: "deviceId",
32407
+ form: "single",
32408
+ optional: false
32409
+ }],
32410
+ "sceneMonitor.createScene": [{
32411
+ name: "deviceId",
32412
+ form: "single",
32413
+ optional: false
32414
+ }],
32415
+ "sceneMonitor.deleteReference": [{
32416
+ name: "deviceId",
32417
+ form: "single",
32418
+ optional: false
32419
+ }],
32420
+ "sceneMonitor.deleteScene": [{
32421
+ name: "deviceId",
32422
+ form: "single",
32423
+ optional: false
32424
+ }],
32425
+ "sceneMonitor.listScenes": [{
32426
+ name: "deviceId",
32427
+ form: "single",
32428
+ optional: false
32429
+ }],
32430
+ "sceneMonitor.recheckNow": [{
32431
+ name: "deviceId",
32432
+ form: "single",
32433
+ optional: false
32434
+ }],
32435
+ "sceneMonitor.resetScene": [{
32436
+ name: "deviceId",
32437
+ form: "single",
32438
+ optional: false
32439
+ }],
32440
+ "sceneMonitor.updateScene": [{
32441
+ name: "deviceId",
32442
+ form: "single",
32443
+ optional: false
32444
+ }],
32445
+ "scriptRunner.run": [{
32446
+ name: "deviceId",
32447
+ form: "single",
32448
+ optional: false
32449
+ }],
32450
+ "scriptRunner.stop": [{
32451
+ name: "deviceId",
32452
+ form: "single",
32453
+ optional: false
32454
+ }],
32455
+ "snapshot.getSnapshot": [{
32456
+ name: "deviceId",
32457
+ form: "single",
32458
+ optional: false
32459
+ }],
32460
+ "snapshot.getSnapshotOverview": [{
32461
+ name: "deviceIds",
32462
+ form: "array",
32463
+ optional: false
32464
+ }],
32465
+ "snapshot.invalidateCache": [{
32466
+ name: "deviceId",
32467
+ form: "single",
32468
+ optional: false
32469
+ }],
32470
+ "streamBroker.acquireEgressTranscode": [{
32471
+ name: "deviceId",
32472
+ form: "single",
32473
+ optional: false
32474
+ }],
32475
+ "streamBroker.assignProfile": [{
32476
+ name: "deviceId",
32477
+ form: "single",
32478
+ optional: false
32479
+ }],
32480
+ "streamBroker.getDeviceAudioMute": [{
32481
+ name: "deviceId",
32482
+ form: "single",
32483
+ optional: false
32484
+ }],
32485
+ "streamBroker.getStreamWithCodec": [{
32486
+ name: "deviceId",
32487
+ form: "single",
32488
+ optional: false
32489
+ }],
32490
+ "streamBroker.produceEventMedia": [{
32491
+ name: "deviceId",
32492
+ form: "single",
32493
+ optional: false
32494
+ }],
32495
+ "streamBroker.publishCameraStream": [{
32496
+ name: "deviceId",
32497
+ form: "single",
32498
+ optional: false
32499
+ }],
32500
+ "streamBroker.renderPreBufferClip": [{
32501
+ name: "deviceId",
32502
+ form: "single",
32503
+ optional: false
32504
+ }],
32505
+ "streamBroker.restartProfile": [{
32506
+ name: "deviceId",
32507
+ form: "single",
32508
+ optional: false
32509
+ }],
32510
+ "streamBroker.retractCameraStream": [{
32511
+ name: "deviceId",
32512
+ form: "single",
32513
+ optional: false
32514
+ }],
32515
+ "streamBroker.setDeviceAudioMute": [{
32516
+ name: "deviceId",
32517
+ form: "single",
32518
+ optional: false
32519
+ }],
32520
+ "streamBroker.unassignProfile": [{
32521
+ name: "deviceId",
32522
+ form: "single",
32523
+ optional: false
32524
+ }],
32525
+ "streamCatalog.getCatalog": [{
32526
+ name: "deviceId",
32527
+ form: "single",
32528
+ optional: false
32529
+ }],
32530
+ "streamParams.getConfigSchema": [{
32531
+ name: "deviceId",
32532
+ form: "single",
32533
+ optional: false
32534
+ }],
32535
+ "streamParams.getOptions": [{
32536
+ name: "deviceId",
32537
+ form: "single",
32538
+ optional: false
32539
+ }],
32540
+ "streamParams.setProfile": [{
32541
+ name: "deviceId",
32542
+ form: "single",
32543
+ optional: false
32544
+ }],
32545
+ "switch.setState": [{
32546
+ name: "deviceId",
32547
+ form: "single",
32548
+ optional: false
32549
+ }],
32550
+ "vacuumControl.locate": [{
32551
+ name: "deviceId",
32552
+ form: "single",
32553
+ optional: false
32554
+ }],
32555
+ "vacuumControl.pause": [{
32556
+ name: "deviceId",
32557
+ form: "single",
32558
+ optional: false
32559
+ }],
32560
+ "vacuumControl.returnToBase": [{
32561
+ name: "deviceId",
32562
+ form: "single",
32563
+ optional: false
32564
+ }],
32565
+ "vacuumControl.setFanSpeed": [{
32566
+ name: "deviceId",
32567
+ form: "single",
32568
+ optional: false
32569
+ }],
32570
+ "vacuumControl.start": [{
32571
+ name: "deviceId",
32572
+ form: "single",
32573
+ optional: false
32574
+ }],
32575
+ "vacuumControl.stop": [{
32576
+ name: "deviceId",
32577
+ form: "single",
32578
+ optional: false
32579
+ }],
32580
+ "valve.close": [{
32581
+ name: "deviceId",
32582
+ form: "single",
32583
+ optional: false
32584
+ }],
32585
+ "valve.open": [{
32586
+ name: "deviceId",
32587
+ form: "single",
32588
+ optional: false
32589
+ }],
32590
+ "valve.setPosition": [{
32591
+ name: "deviceId",
32592
+ form: "single",
32593
+ optional: false
32594
+ }],
32595
+ "valve.stop": [{
32596
+ name: "deviceId",
32597
+ form: "single",
32598
+ optional: false
32599
+ }],
32600
+ "videoclips.getClipPlayback": [{
32601
+ name: "deviceId",
32602
+ form: "single",
32603
+ optional: false
32604
+ }],
32605
+ "videoclips.listClips": [{
32606
+ name: "deviceId",
32607
+ form: "single",
32608
+ optional: false
32609
+ }],
32610
+ "waterHeater.setAway": [{
32611
+ name: "deviceId",
32612
+ form: "single",
32613
+ optional: false
32614
+ }],
32615
+ "waterHeater.setOperationMode": [{
32616
+ name: "deviceId",
32617
+ form: "single",
32618
+ optional: false
32619
+ }],
32620
+ "waterHeater.setTargetTemp": [{
32621
+ name: "deviceId",
32622
+ form: "single",
32623
+ optional: false
32624
+ }],
32625
+ "webrtcSession.addIceCandidate": [{
32626
+ name: "deviceId",
32627
+ form: "single",
32628
+ optional: false
32629
+ }],
32630
+ "webrtcSession.closeSession": [{
32631
+ name: "deviceId",
32632
+ form: "single",
32633
+ optional: false
32634
+ }],
32635
+ "webrtcSession.createSession": [{
32636
+ name: "deviceId",
32637
+ form: "single",
32638
+ optional: false
32639
+ }],
32640
+ "webrtcSession.getIceCandidates": [{
32641
+ name: "deviceId",
32642
+ form: "single",
32643
+ optional: false
32644
+ }],
32645
+ "webrtcSession.getSessionState": [{
32646
+ name: "deviceId",
32647
+ form: "single",
32648
+ optional: false
32649
+ }],
32650
+ "webrtcSession.handleAnswer": [{
32651
+ name: "deviceId",
32652
+ form: "single",
32653
+ optional: false
32654
+ }],
32655
+ "webrtcSession.handleOffer": [{
32656
+ name: "deviceId",
32657
+ form: "single",
32658
+ optional: false
32659
+ }],
32660
+ "webrtcSession.hasAdaptiveBitrate": [{
32661
+ name: "deviceId",
32662
+ form: "single",
32663
+ optional: false
32664
+ }],
32665
+ "webrtcSession.listStreams": [{
32666
+ name: "deviceId",
32667
+ form: "single",
32668
+ optional: false
32669
+ }],
32670
+ "zoneAnalytics.getCameraHistory": [{
32671
+ name: "deviceId",
32672
+ form: "single",
32673
+ optional: false
32674
+ }],
32675
+ "zoneAnalytics.getCurrentSnapshot": [{
32676
+ name: "deviceId",
32677
+ form: "single",
32678
+ optional: false
32679
+ }],
32680
+ "zoneAnalytics.getUnzonedHistory": [{
32681
+ name: "deviceId",
32682
+ form: "single",
32683
+ optional: false
32684
+ }],
32685
+ "zoneAnalytics.getZoneHistory": [{
32686
+ name: "deviceId",
32687
+ form: "single",
32688
+ optional: false
32689
+ }],
32690
+ "zoneRules.listRules": [{
32691
+ name: "deviceId",
32692
+ form: "single",
32693
+ optional: false
32694
+ }],
32695
+ "zoneRules.setRules": [{
32696
+ name: "deviceId",
32697
+ form: "single",
32698
+ optional: false
32699
+ }],
32700
+ "zones.addZone": [{
32701
+ name: "deviceId",
32702
+ form: "single",
32703
+ optional: false
32704
+ }],
32705
+ "zones.listZones": [{
32706
+ name: "deviceId",
32707
+ form: "single",
32708
+ optional: false
32709
+ }],
32710
+ "zones.removeZone": [{
32711
+ name: "deviceId",
32712
+ form: "single",
32713
+ optional: false
32714
+ }],
32715
+ "zones.updateZone": [{
32716
+ name: "deviceId",
32717
+ form: "single",
32718
+ optional: false
32719
+ }]
32720
+ });
30605
32721
  Object.freeze({
30606
32722
  "broker": "broker",
30607
32723
  "device-export": "device-export",
@@ -31029,7 +33145,7 @@ function mapHttpError(res) {
31029
33145
  };
31030
33146
  }
31031
33147
  /** Narrow an unknown to a plain object record (documented JSON type boundary). */
31032
- function asRecord(value) {
33148
+ function asRecord$1(value) {
31033
33149
  if (value !== null && typeof value === "object" && !Array.isArray(value)) return value;
31034
33150
  return null;
31035
33151
  }
@@ -31156,10 +33272,10 @@ function buildChatCompletionsBody(profile, request) {
31156
33272
  };
31157
33273
  return body;
31158
33274
  }
31159
- function readString$2(value) {
33275
+ function readString$3(value) {
31160
33276
  return typeof value === "string" ? value : void 0;
31161
33277
  }
31162
- function readNumber$2(value) {
33278
+ function readNumber$3(value) {
31163
33279
  return typeof value === "number" && Number.isFinite(value) ? value : 0;
31164
33280
  }
31165
33281
  function parseChatCompletionsResult(res, latencyMs) {
@@ -31171,33 +33287,33 @@ function parseChatCompletionsResult(res, latencyMs) {
31171
33287
  code: "adapter-error",
31172
33288
  message: "invalid JSON response"
31173
33289
  };
31174
- const choice = asRecord((Array.isArray(body.choices) ? body.choices : [])[0]);
33290
+ const choice = asRecord$1((Array.isArray(body.choices) ? body.choices : [])[0]);
31175
33291
  if (choice === null) return {
31176
33292
  ok: false,
31177
33293
  code: "adapter-error",
31178
33294
  message: "missing choices[0]"
31179
33295
  };
31180
- const finishReason = readString$2(choice.finish_reason);
33296
+ const finishReason = readString$3(choice.finish_reason);
31181
33297
  if (finishReason === "content_filter") return {
31182
33298
  ok: false,
31183
33299
  code: "refusal",
31184
33300
  message: "content filtered"
31185
33301
  };
31186
- const messageObj = asRecord(choice.message);
31187
- const text = messageObj !== null ? readString$2(messageObj.content) : void 0;
33302
+ const messageObj = asRecord$1(choice.message);
33303
+ const text = messageObj !== null ? readString$3(messageObj.content) : void 0;
31188
33304
  if (text === void 0) return {
31189
33305
  ok: false,
31190
33306
  code: "adapter-error",
31191
33307
  message: "missing message.content"
31192
33308
  };
31193
- const usageObj = asRecord(body.usage) ?? {};
33309
+ const usageObj = asRecord$1(body.usage) ?? {};
31194
33310
  return {
31195
33311
  ok: true,
31196
33312
  text,
31197
- model: readString$2(body.model) ?? "",
33313
+ model: readString$3(body.model) ?? "",
31198
33314
  usage: {
31199
- inputTokens: readNumber$2(usageObj.prompt_tokens),
31200
- outputTokens: readNumber$2(usageObj.completion_tokens)
33315
+ inputTokens: readNumber$3(usageObj.prompt_tokens),
33316
+ outputTokens: readNumber$3(usageObj.completion_tokens)
31201
33317
  },
31202
33318
  truncated: finishReason === "length",
31203
33319
  latencyMs
@@ -31208,7 +33324,7 @@ function parseModelsList(res) {
31208
33324
  const data = body !== null && Array.isArray(body.data) ? body.data : [];
31209
33325
  const ids = [];
31210
33326
  for (const entry of data) if (entry !== null && typeof entry === "object") {
31211
- const id = readString$2(entry.id);
33327
+ const id = readString$3(entry.id);
31212
33328
  if (id !== void 0) ids.push(id);
31213
33329
  }
31214
33330
  return ids;
@@ -31274,10 +33390,10 @@ function userContent(request) {
31274
33390
  });
31275
33391
  return parts;
31276
33392
  }
31277
- function readString$1(value) {
33393
+ function readString$2(value) {
31278
33394
  return typeof value === "string" ? value : void 0;
31279
33395
  }
31280
- function readNumber$1(value) {
33396
+ function readNumber$2(value) {
31281
33397
  return typeof value === "number" && Number.isFinite(value) ? value : 0;
31282
33398
  }
31283
33399
  function buildMessagesBody(profile, request) {
@@ -31314,7 +33430,7 @@ function parseMessagesResult(res, latencyMs) {
31314
33430
  code: "adapter-error",
31315
33431
  message: "invalid JSON response"
31316
33432
  };
31317
- const stopReason = readString$1(body.stop_reason);
33433
+ const stopReason = readString$2(body.stop_reason);
31318
33434
  if (stopReason === "refusal") return {
31319
33435
  ok: false,
31320
33436
  code: "refusal",
@@ -31329,7 +33445,7 @@ function parseMessagesResult(res, latencyMs) {
31329
33445
  text = JSON.stringify(b.input);
31330
33446
  break;
31331
33447
  }
31332
- if (b.type === "text" && text === void 0) text = readString$1(b.text);
33448
+ if (b.type === "text" && text === void 0) text = readString$2(b.text);
31333
33449
  }
31334
33450
  if (text === void 0) return {
31335
33451
  ok: false,
@@ -31340,10 +33456,10 @@ function parseMessagesResult(res, latencyMs) {
31340
33456
  return {
31341
33457
  ok: true,
31342
33458
  text,
31343
- model: readString$1(body.model) ?? "",
33459
+ model: readString$2(body.model) ?? "",
31344
33460
  usage: {
31345
- inputTokens: readNumber$1(usageObj.input_tokens),
31346
- outputTokens: readNumber$1(usageObj.output_tokens)
33461
+ inputTokens: readNumber$2(usageObj.input_tokens),
33462
+ outputTokens: readNumber$2(usageObj.output_tokens)
31347
33463
  },
31348
33464
  truncated: stopReason === "max_tokens",
31349
33465
  latencyMs
@@ -31378,7 +33494,7 @@ var anthropicAdapter = {
31378
33494
  const data = body !== null && Array.isArray(body.data) ? body.data : [];
31379
33495
  const ids = [];
31380
33496
  for (const entry of data) if (entry !== null && typeof entry === "object") {
31381
- const id = readString$1(entry.id);
33497
+ const id = readString$2(entry.id);
31382
33498
  if (id !== void 0) ids.push(id);
31383
33499
  }
31384
33500
  return ids;
@@ -31396,10 +33512,10 @@ function headers(profile) {
31396
33512
  ...profile.extraHeaders
31397
33513
  };
31398
33514
  }
31399
- function readString(value) {
33515
+ function readString$1(value) {
31400
33516
  return typeof value === "string" ? value : void 0;
31401
33517
  }
31402
- function readNumber(value) {
33518
+ function readNumber$1(value) {
31403
33519
  return typeof value === "number" && Number.isFinite(value) ? value : 0;
31404
33520
  }
31405
33521
  function buildGenerateContentBody(profile, request) {
@@ -31434,37 +33550,37 @@ function parseGenerateContentResult(res, latencyMs) {
31434
33550
  code: "adapter-error",
31435
33551
  message: "invalid JSON response"
31436
33552
  };
31437
- const promptFeedback = asRecord(body.promptFeedback);
31438
- if (promptFeedback !== null && readString(promptFeedback.blockReason) !== void 0) return {
33553
+ const promptFeedback = asRecord$1(body.promptFeedback);
33554
+ if (promptFeedback !== null && readString$1(promptFeedback.blockReason) !== void 0) return {
31439
33555
  ok: false,
31440
33556
  code: "refusal",
31441
- message: `blocked: ${readString(promptFeedback.blockReason)}`
33557
+ message: `blocked: ${readString$1(promptFeedback.blockReason)}`
31442
33558
  };
31443
- const candidate = asRecord((Array.isArray(body.candidates) ? body.candidates : [])[0]);
33559
+ const candidate = asRecord$1((Array.isArray(body.candidates) ? body.candidates : [])[0]);
31444
33560
  if (candidate === null) return {
31445
33561
  ok: false,
31446
33562
  code: "adapter-error",
31447
33563
  message: "missing candidates[0]"
31448
33564
  };
31449
- const finishReason = readString(candidate.finishReason);
33565
+ const finishReason = readString$1(candidate.finishReason);
31450
33566
  if (finishReason === "SAFETY" || finishReason === "PROHIBITED_CONTENT") return {
31451
33567
  ok: false,
31452
33568
  code: "refusal",
31453
33569
  message: `finish: ${finishReason}`
31454
33570
  };
31455
- const contentObj = asRecord(candidate.content) ?? {};
33571
+ const contentObj = asRecord$1(candidate.content) ?? {};
31456
33572
  const text = (Array.isArray(contentObj.parts) ? contentObj.parts : []).map((p) => {
31457
- const rec = asRecord(p);
31458
- return rec !== null ? readString(rec.text) : void 0;
33573
+ const rec = asRecord$1(p);
33574
+ return rec !== null ? readString$1(rec.text) : void 0;
31459
33575
  }).filter((t) => t !== void 0).join("");
31460
- const usageObj = asRecord(body.usageMetadata) ?? {};
33576
+ const usageObj = asRecord$1(body.usageMetadata) ?? {};
31461
33577
  return {
31462
33578
  ok: true,
31463
33579
  text,
31464
- model: readString(body.modelVersion) ?? "",
33580
+ model: readString$1(body.modelVersion) ?? "",
31465
33581
  usage: {
31466
- inputTokens: readNumber(usageObj.promptTokenCount),
31467
- outputTokens: readNumber(usageObj.candidatesTokenCount)
33582
+ inputTokens: readNumber$1(usageObj.promptTokenCount),
33583
+ outputTokens: readNumber$1(usageObj.candidatesTokenCount)
31468
33584
  },
31469
33585
  truncated: finishReason === "MAX_TOKENS",
31470
33586
  latencyMs
@@ -31502,7 +33618,7 @@ var googleAdapter = {
31502
33618
  const models = body !== null && Array.isArray(body.models) ? body.models : [];
31503
33619
  const ids = [];
31504
33620
  for (const entry of models) if (entry !== null && typeof entry === "object") {
31505
- const name = readString(entry.name);
33621
+ const name = readString$1(entry.name);
31506
33622
  if (name !== void 0) ids.push(stripModelsPrefix(name));
31507
33623
  }
31508
33624
  return ids;
@@ -32642,7 +34758,7 @@ async function assembleAi(deps) {
32642
34758
  registrations,
32643
34759
  runtimeProvider
32644
34760
  };
32645
- const { UsageStore } = await import("./usage-store-1FrTJdpU.mjs").then((n) => n.i);
34761
+ const { UsageStore } = await import("./usage-store-CsJWTWNX.mjs").then((n) => n.i);
32646
34762
  const store = new ProfileStore(deps.settingsPort);
32647
34763
  const defaults = new DefaultsStore(deps.settingsPort);
32648
34764
  const usage = new UsageStore(deps.settingsPort, Date.now, deps.logger.child("llm-usage"));
@@ -32675,10 +34791,843 @@ async function assembleAi(deps) {
32675
34791
  runtimeProvider,
32676
34792
  llmProvider,
32677
34793
  store,
34794
+ usage,
32678
34795
  prune: (retentionDays) => usage.prune(retentionDays)
32679
34796
  };
32680
34797
  }
32681
34798
  //#endregion
34799
+ //#region src/test-chat/chat-completions-stream.ts
34800
+ /** The profile kinds whose wire this module speaks. */
34801
+ var STREAMABLE_KINDS = new Set(["openai-compatible", "openai"]);
34802
+ var OPENAI_DEFAULT_BASE_URL = "https://api.openai.com/v1";
34803
+ function baseUrlFor(profile) {
34804
+ if (profile.baseUrl !== void 0 && profile.baseUrl.length > 0) return trimTrailingSlash(profile.baseUrl);
34805
+ return profile.kind === "openai" ? OPENAI_DEFAULT_BASE_URL : null;
34806
+ }
34807
+ function readString(value) {
34808
+ return typeof value === "string" ? value : void 0;
34809
+ }
34810
+ function readNumber(value) {
34811
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
34812
+ }
34813
+ function asRecord(value) {
34814
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
34815
+ }
34816
+ /**
34817
+ * Split a byte stream into SSE `data:` payloads.
34818
+ *
34819
+ * Chunk boundaries fall wherever TCP puts them, so a frame is buffered until
34820
+ * its newline arrives — reading a half-written JSON object is the classic way
34821
+ * a streaming client "works" until the first slow link.
34822
+ */
34823
+ async function* sseData(stream) {
34824
+ const decoder = new TextDecoder();
34825
+ let buffer = "";
34826
+ for await (const chunk of stream) {
34827
+ buffer += decoder.decode(chunk, { stream: true });
34828
+ let index = buffer.indexOf("\n");
34829
+ while (index >= 0) {
34830
+ const line = buffer.slice(0, index).replace(/\r$/, "");
34831
+ buffer = buffer.slice(index + 1);
34832
+ if (line.startsWith("data:")) yield line.slice(5).trim();
34833
+ index = buffer.indexOf("\n");
34834
+ }
34835
+ }
34836
+ const tail = buffer.replace(/\r$/, "");
34837
+ if (tail.startsWith("data:")) yield tail.slice(5).trim();
34838
+ }
34839
+ /** SSE payloads → normalised chunks. Ends at `[DONE]` or stream end. */
34840
+ async function* chunksFromSse(payloads) {
34841
+ let inputTokens = 0;
34842
+ let outputTokens = 0;
34843
+ let truncated = false;
34844
+ for await (const payload of payloads) {
34845
+ if (payload === "") continue;
34846
+ if (payload === "[DONE]") break;
34847
+ let parsed;
34848
+ try {
34849
+ parsed = JSON.parse(payload);
34850
+ } catch {
34851
+ continue;
34852
+ }
34853
+ const body = asRecord(parsed);
34854
+ if (body === null) continue;
34855
+ const usage = asRecord(body.usage);
34856
+ if (usage !== null) {
34857
+ inputTokens = readNumber(usage.prompt_tokens);
34858
+ outputTokens = readNumber(usage.completion_tokens);
34859
+ }
34860
+ const choice = asRecord((Array.isArray(body.choices) ? body.choices : [])[0]);
34861
+ if (choice === null) continue;
34862
+ if (readString(choice.finish_reason) === "length") truncated = true;
34863
+ const delta = asRecord(choice.delta);
34864
+ const text = delta !== null ? readString(delta.content) : void 0;
34865
+ if (text !== void 0 && text.length > 0) yield {
34866
+ kind: "token",
34867
+ text
34868
+ };
34869
+ }
34870
+ yield {
34871
+ kind: "end",
34872
+ inputTokens,
34873
+ outputTokens,
34874
+ truncated
34875
+ };
34876
+ }
34877
+ /**
34878
+ * Open the streamed completion.
34879
+ *
34880
+ * The connect bound covers ONLY the wait for response headers — once the
34881
+ * provider has answered, the first-token and idle bounds take over upstream.
34882
+ * They are separated because "the port is closed" and "the model is loading"
34883
+ * deserve different patience and different sentences.
34884
+ */
34885
+ function createChatCompletionsStreamer(deps = {}) {
34886
+ const fetchImpl = deps.fetchImpl ?? fetch;
34887
+ return async (request, opts) => {
34888
+ const profile = request.profile;
34889
+ if (!STREAMABLE_KINDS.has(profile.kind)) return { kind: "unsupported" };
34890
+ const base = baseUrlFor(profile);
34891
+ if (base === null) return { kind: "unsupported" };
34892
+ const body = {
34893
+ ...buildChatCompletionsBody(profile, {
34894
+ prompt: request.prompt,
34895
+ ...request.system !== void 0 ? { system: request.system } : {},
34896
+ ...request.images !== void 0 ? { images: request.images } : {},
34897
+ ...request.maxTokens !== void 0 ? { maxTokens: request.maxTokens } : {},
34898
+ ...request.temperature !== void 0 ? { temperature: request.temperature } : {}
34899
+ }),
34900
+ stream: true,
34901
+ stream_options: { include_usage: true }
34902
+ };
34903
+ const connectController = new AbortController();
34904
+ const onOuterAbort = () => connectController.abort();
34905
+ opts.signal.addEventListener("abort", onOuterAbort, { once: true });
34906
+ const connectTimer = setTimeout(() => connectController.abort(), opts.connectTimeoutMs);
34907
+ connectTimer.unref?.();
34908
+ let res;
34909
+ try {
34910
+ res = await fetchImpl(`${base}/chat/completions`, {
34911
+ method: "POST",
34912
+ headers: {
34913
+ ...authHeaders(profile),
34914
+ accept: "text/event-stream"
34915
+ },
34916
+ body: JSON.stringify(body),
34917
+ signal: connectController.signal
34918
+ });
34919
+ } catch (cause) {
34920
+ if (cause instanceof Error && cause.name === "AbortError" && !opts.signal.aborted) return {
34921
+ kind: "connect-timeout",
34922
+ timeoutMs: opts.connectTimeoutMs
34923
+ };
34924
+ return {
34925
+ kind: "network",
34926
+ message: cause instanceof Error ? cause.message : String(cause)
34927
+ };
34928
+ } finally {
34929
+ clearTimeout(connectTimer);
34930
+ opts.signal.removeEventListener("abort", onOuterAbort);
34931
+ }
34932
+ if (!res.ok) {
34933
+ const detail = (await res.text().catch(() => "")).slice(0, 500);
34934
+ return {
34935
+ kind: "network",
34936
+ message: `HTTP ${String(res.status)}${detail ? `: ${detail}` : ""}`
34937
+ };
34938
+ }
34939
+ if (res.body === null) return {
34940
+ kind: "network",
34941
+ message: "the provider returned no body"
34942
+ };
34943
+ const bytes = res.body;
34944
+ return {
34945
+ kind: "open",
34946
+ chunks: chunksFromSse(sseData(bytes))
34947
+ };
34948
+ };
34949
+ }
34950
+ /** Per-message cap — the prompt is flattened, so this bounds the whole call. */
34951
+ var TEST_CHAT_MAX_CONTENT_CHARS = 8e3;
34952
+ /** The data-plane prefix under `/addon/ai/`. */
34953
+ var TEST_CHAT_PREFIX = "test-chat";
34954
+ /** The usage tag every test turn is billed under (`llm.getUsage`). */
34955
+ var TEST_CHAT_CONSUMER = "adhoc-ui";
34956
+ var TEST_CHAT_CONNECT_TIMEOUT_MS = 1e4;
34957
+ var TEST_CHAT_DEFAULT_FIRST_TOKEN_TIMEOUT_MS = 12e4;
34958
+ var TEST_CHAT_MAX_FIRST_TOKEN_TIMEOUT_MS = 6e5;
34959
+ var TEST_CHAT_IDLE_TIMEOUT_MS = 6e4;
34960
+ var TestChatMessageSchema = object({
34961
+ role: _enum(["user", "assistant"]),
34962
+ content: string().max(TEST_CHAT_MAX_CONTENT_CHARS)
34963
+ });
34964
+ /**
34965
+ * WHAT to look at, never the pixels themselves.
34966
+ *
34967
+ * `snapshot` is the camera's current frame through the snapshot wrapper (cache
34968
+ * + coalescing + the battery-camera sleep gate all still apply — this is a test
34969
+ * surface and has no business setting `force`).
34970
+ *
34971
+ * `track` is a stored best-shot: the addon picks ONE media row the pipeline
34972
+ * already wrote (see `TRACK_MEDIA_PREFERENCE`) and sends it verbatim. No crop
34973
+ * rectangle is computed here — D52 keeps that in the runner, and a test chat
34974
+ * that derived its own would be judging a rectangle nothing else ever sees.
34975
+ */
34976
+ var TestChatImageRefSchema = discriminatedUnion("kind", [object({
34977
+ kind: literal("snapshot"),
34978
+ deviceId: number().int().positive()
34979
+ }), object({
34980
+ kind: literal("track"),
34981
+ trackId: string().min(1)
34982
+ })]);
34983
+ /**
34984
+ * `.strict()`, and it is load-bearing.
34985
+ *
34986
+ * Measured on the live hub: a request that named the attachment field
34987
+ * `attachment` against a schema that called it `imageRef` was accepted, the
34988
+ * unknown key was STRIPPED by the default object parse, and the run answered a
34989
+ * vision question with a text-only turn — "specify what you want described",
34990
+ * `inputTokens: 10`. No error, no log, no way to tell it from a model that
34991
+ * simply could not see. A stripped field is a silent behaviour change, which is
34992
+ * the one thing a diagnostic surface must never do; `.strict()` turns it into a
34993
+ * 400 naming the key.
34994
+ */
34995
+ var TestChatRequestSchema = object({
34996
+ /** Explicit profile — the test chat NEVER falls through to the default
34997
+ * resolution chain, because "which model answered" is the whole question. */
34998
+ profileId: string().min(1),
34999
+ messages: array(TestChatMessageSchema).min(1).max(40),
35000
+ /** Optional system turn, authored by the operator in the page. */
35001
+ system: string().max(TEST_CHAT_MAX_CONTENT_CHARS).optional(),
35002
+ /** The picture to look at, BY REFERENCE. Named `attachment` because that is
35003
+ * what every caller reaches for; the previous `imageRef` cost a live
35004
+ * debugging session to the strip described above. */
35005
+ attachment: TestChatImageRefSchema.optional(),
35006
+ temperature: number().min(0).max(2).optional(),
35007
+ maxTokens: number().int().positive().max(8192).optional(),
35008
+ /** See the timeout block above. Applies to a VISION turn identically —
35009
+ * vision inference is the slower one, so a bound tuned on text is a bound
35010
+ * that only fails on the interesting case. */
35011
+ firstTokenTimeoutMs: number().int().positive().max(TEST_CHAT_MAX_FIRST_TOKEN_TIMEOUT_MS).default(TEST_CHAT_DEFAULT_FIRST_TOKEN_TIMEOUT_MS)
35012
+ }).strict();
35013
+ /**
35014
+ * Below this, a turn that CARRIED an image almost certainly did not deliver it.
35015
+ *
35016
+ * A vision model prices a picture in hundreds of input tokens (qwen3-vl bills a
35017
+ * 480 px crop at several hundred); a text-only turn of a short prompt bills
35018
+ * about ten. The live failure above reported exactly `10`. So when the provider
35019
+ * tells us the input count and an image was attached, a count this low is
35020
+ * evidence the picture did not reach the model — worth a warning line even
35021
+ * though the run "succeeded", because the answer will look like an answer.
35022
+ *
35023
+ * Only checked when the provider actually reported a count: a streamed
35024
+ * completion without `stream_options.include_usage` reports none, and 0 must
35025
+ * never be read as 10.
35026
+ */
35027
+ var TEST_CHAT_MIN_VISION_INPUT_TOKENS = 100;
35028
+ /**
35029
+ * What the server actually attached — metadata only, never the bytes.
35030
+ *
35031
+ * The operator has to be able to tell a vision answer about the wrong picture
35032
+ * from a vision answer about the right one, and the only way to do that from
35033
+ * the client is to be told which media row was used and how old it is.
35034
+ */
35035
+ var TestChatAttachmentSchema = object({
35036
+ kind: _enum(["snapshot", "track"]),
35037
+ mimeType: string(),
35038
+ sizeBytes: number().int().nonnegative(),
35039
+ /** Track attachments only: the `MediaFileKind` that won `TRACK_MEDIA_PREFERENCE`. */
35040
+ mediaKind: string().optional(),
35041
+ /** Epoch ms of the frame, when the source knows it. */
35042
+ capturedAt: number().optional()
35043
+ });
35044
+ /**
35045
+ * What the run is doing right now — so a wait reads as progress, not as a hang.
35046
+ *
35047
+ * `first-token-wait` is the one that matters: it is the phase a cold model
35048
+ * spends minutes in, and the page renders it as "the model is loading…" rather
35049
+ * than as silence.
35050
+ */
35051
+ var TestChatPhaseSchema = _enum([
35052
+ "resolving-image",
35053
+ "connecting",
35054
+ "first-token-wait",
35055
+ "streaming"
35056
+ ]);
35057
+ /**
35058
+ * One NDJSON line. A discriminated union so the client narrows on `kind` and a
35059
+ * line this build does not understand is skipped rather than half-read.
35060
+ *
35061
+ * `meta` arrives BEFORE the first token: it is what the server committed to —
35062
+ * which profile, which model, which picture — and the page shows it while the
35063
+ * model is still loading, so a long wait is at least an informed one.
35064
+ */
35065
+ var TestChatEventSchema = discriminatedUnion("kind", [
35066
+ object({
35067
+ kind: literal("status"),
35068
+ phase: TestChatPhaseSchema
35069
+ }),
35070
+ object({
35071
+ kind: literal("meta"),
35072
+ profileName: string(),
35073
+ model: string(),
35074
+ /**
35075
+ * WHAT MEDIA WAS ACTUALLY SENT — and the contract is that this is non-null
35076
+ * whenever the request carried an `attachment`.
35077
+ *
35078
+ * There is no path where a requested picture goes missing and the run
35079
+ * continues: a resolution failure ends the stream with an `error` event.
35080
+ * That rule exists because the opposite shipped — a vision question answered
35081
+ * text-only, indistinguishable from a model that looked and saw nothing.
35082
+ */
35083
+ mediaSent: TestChatAttachmentSchema.nullable(),
35084
+ /** False when the profile's wire has no token stream and the whole answer
35085
+ * arrives at once. Said out loud rather than faked. */
35086
+ streamed: boolean(),
35087
+ /** Echoed so the page states the bound it is really working under. */
35088
+ firstTokenTimeoutMs: number()
35089
+ }),
35090
+ object({
35091
+ kind: literal("token"),
35092
+ text: string()
35093
+ }),
35094
+ object({
35095
+ kind: literal("done"),
35096
+ inputTokens: number(),
35097
+ outputTokens: number(),
35098
+ latencyMs: number(),
35099
+ truncated: boolean()
35100
+ }),
35101
+ object({
35102
+ kind: literal("error"),
35103
+ code: string(),
35104
+ message: string()
35105
+ })
35106
+ ]);
35107
+ /** Serialise one event as an NDJSON line, terminator included. */
35108
+ function encodeEvent(event) {
35109
+ return `${JSON.stringify(event)}\n`;
35110
+ }
35111
+ //#endregion
35112
+ //#region src/test-chat/media.ts
35113
+ /**
35114
+ * Which stored media a track attachment is allowed to be, best first.
35115
+ *
35116
+ * `keyFrameSmall` wins because it is the CLEAN best-shot (no drawn box) at a
35117
+ * size a vision model actually ingests — a boxed frame teaches the model to
35118
+ * describe the annotation, and the native `keyFrame` is megabytes for no gain.
35119
+ * The subject crops come next for a track whose key frame was never written,
35120
+ * and `fullFrameBoxed` is absent on purpose: it is the one variant with
35121
+ * graphics painted over the evidence.
35122
+ */
35123
+ var TRACK_MEDIA_PREFERENCE = [
35124
+ "keyFrameSmall",
35125
+ "keyFrame",
35126
+ "thumbnailSmall",
35127
+ "thumbnail",
35128
+ "crop"
35129
+ ];
35130
+ /** JPEG is what every media row and every snapshot in this system is. */
35131
+ var DEFAULT_MIME = "image/jpeg";
35132
+ /**
35133
+ * Flatten the transcript into the single prompt the chat-completions wire takes
35134
+ * as one user turn.
35135
+ *
35136
+ * A SINGLE user turn is sent verbatim: prefixing one message with `User:`
35137
+ * teaches small local models to answer in the same transcript format, which
35138
+ * makes the very first test a confusing one.
35139
+ */
35140
+ function renderTranscript(messages) {
35141
+ if (messages.length === 1 && messages[0].role === "user") return messages[0].content;
35142
+ return messages.map((m) => `${m.role === "user" ? "User" : "Assistant"}: ${m.content}`).join("\n\n");
35143
+ }
35144
+ /** The highest-preference media row present, or null when none qualifies. */
35145
+ function pickTrackMedia(rows) {
35146
+ for (const kind of TRACK_MEDIA_PREFERENCE) {
35147
+ const hit = rows.find((r) => r.kind === kind);
35148
+ if (hit !== void 0) return hit;
35149
+ }
35150
+ return null;
35151
+ }
35152
+ function decode(base64) {
35153
+ const buf = Buffer.from(base64, "base64");
35154
+ const bytes = new Uint8Array(new ArrayBuffer(buf.byteLength));
35155
+ bytes.set(buf);
35156
+ return bytes;
35157
+ }
35158
+ async function resolveImage(deps, ref) {
35159
+ if (ref.kind === "snapshot") {
35160
+ const tags = { deviceId: ref.deviceId };
35161
+ let shot;
35162
+ try {
35163
+ shot = await deps.getSnapshot(ref.deviceId);
35164
+ } catch (cause) {
35165
+ const message = cause instanceof Error ? cause.message : String(cause);
35166
+ deps.logger.warn("ai test chat: snapshot read failed — turn dropped", {
35167
+ tags,
35168
+ meta: { error: message }
35169
+ });
35170
+ return {
35171
+ ok: false,
35172
+ failure: {
35173
+ code: "unavailable",
35174
+ message: `snapshot unavailable: ${message}`
35175
+ }
35176
+ };
35177
+ }
35178
+ if (shot === null) {
35179
+ deps.logger.warn("ai test chat: camera has no frame — turn dropped", { tags });
35180
+ return {
35181
+ ok: false,
35182
+ failure: {
35183
+ code: "unavailable",
35184
+ message: "this camera has produced no frame yet"
35185
+ }
35186
+ };
35187
+ }
35188
+ const bytes = decode(shot.base64);
35189
+ const mimeType = shot.contentType.length > 0 ? shot.contentType : DEFAULT_MIME;
35190
+ return {
35191
+ ok: true,
35192
+ resolved: {
35193
+ image: {
35194
+ bytes,
35195
+ mimeType
35196
+ },
35197
+ attachment: {
35198
+ kind: "snapshot",
35199
+ mimeType,
35200
+ sizeBytes: bytes.byteLength
35201
+ }
35202
+ }
35203
+ };
35204
+ }
35205
+ let rows;
35206
+ try {
35207
+ rows = await deps.getTrackMedia(ref.trackId, TRACK_MEDIA_PREFERENCE);
35208
+ } catch (cause) {
35209
+ const message = cause instanceof Error ? cause.message : String(cause);
35210
+ deps.logger.warn("ai test chat: track media read failed — turn dropped", { meta: {
35211
+ trackId: ref.trackId,
35212
+ error: message
35213
+ } });
35214
+ return {
35215
+ ok: false,
35216
+ failure: {
35217
+ code: "unavailable",
35218
+ message: `track media unavailable: ${message}`
35219
+ }
35220
+ };
35221
+ }
35222
+ const row = pickTrackMedia(rows);
35223
+ if (row === null) {
35224
+ deps.logger.warn("ai test chat: track has no usable media — turn dropped", { meta: {
35225
+ trackId: ref.trackId,
35226
+ kindsFound: rows.map((r) => r.kind)
35227
+ } });
35228
+ return {
35229
+ ok: false,
35230
+ failure: {
35231
+ code: "unavailable",
35232
+ message: "this track has no stored best-shot left (evicted or never written)"
35233
+ }
35234
+ };
35235
+ }
35236
+ const bytes = decode(row.base64);
35237
+ return {
35238
+ ok: true,
35239
+ resolved: {
35240
+ image: {
35241
+ bytes,
35242
+ mimeType: DEFAULT_MIME
35243
+ },
35244
+ attachment: {
35245
+ kind: "track",
35246
+ mimeType: DEFAULT_MIME,
35247
+ sizeBytes: bytes.byteLength,
35248
+ mediaKind: row.kind,
35249
+ capturedAt: row.timestamp
35250
+ }
35251
+ }
35252
+ };
35253
+ }
35254
+ //#endregion
35255
+ //#region src/test-chat/run-stream.ts
35256
+ /** Race `promise` against a timer; `null` means the timer won. */
35257
+ async function withDeadline(promise, ms) {
35258
+ let timer;
35259
+ const expiry = new Promise((resolve) => {
35260
+ timer = setTimeout(() => resolve({ timedOut: true }), ms);
35261
+ timer.unref?.();
35262
+ });
35263
+ try {
35264
+ return await Promise.race([promise.then((value) => ({
35265
+ timedOut: false,
35266
+ value
35267
+ })), expiry]);
35268
+ } finally {
35269
+ if (timer !== void 0) clearTimeout(timer);
35270
+ }
35271
+ }
35272
+ /**
35273
+ * Pull the next chunk under a deadline.
35274
+ *
35275
+ * The iterator is NOT abandoned on a timeout — the caller stops reading and the
35276
+ * signal tears the socket down, which is what actually frees the provider. A
35277
+ * dangling `next()` promise here would keep the generator alive.
35278
+ */
35279
+ async function nextChunk(iterator, ms) {
35280
+ const raced = await withDeadline(iterator.next(), ms);
35281
+ if (raced.timedOut) return { kind: "timeout" };
35282
+ return raced.value.done === true ? { kind: "end" } : {
35283
+ kind: "chunk",
35284
+ value: raced.value.value
35285
+ };
35286
+ }
35287
+ async function runTestChatStream(deps, request, emit, signal) {
35288
+ const startedAt = deps.now();
35289
+ const tags = request.attachment?.kind === "snapshot" ? { deviceId: request.attachment.deviceId } : void 0;
35290
+ const withTags = (meta) => tags !== void 0 ? {
35291
+ tags,
35292
+ meta
35293
+ } : { meta };
35294
+ let profileForUsage = null;
35295
+ let imageCount = 0;
35296
+ let finished = false;
35297
+ const finish = async (ending, counts) => {
35298
+ if (finished) return;
35299
+ finished = true;
35300
+ const latencyMs = deps.now() - startedAt;
35301
+ await deps.recordUsage({
35302
+ ts: startedAt,
35303
+ consumer: TEST_CHAT_CONSUMER,
35304
+ ...profileForUsage !== null ? {
35305
+ profileId: profileForUsage.id,
35306
+ kind: profileForUsage.kind,
35307
+ model: profileForUsage.model
35308
+ } : {},
35309
+ inputTokens: counts.inputTokens,
35310
+ outputTokens: counts.outputTokens,
35311
+ latencyMs,
35312
+ ok: ending === null,
35313
+ ...ending !== null ? { errorCode: ending.code } : {},
35314
+ imageCount
35315
+ }).catch(() => void 0);
35316
+ if (ending !== null) emit({
35317
+ kind: "error",
35318
+ code: ending.code,
35319
+ message: ending.message
35320
+ });
35321
+ };
35322
+ const fail = async (ending, logMessage, meta = {}) => {
35323
+ deps.logger.warn(logMessage, withTags({
35324
+ profileId: request.profileId,
35325
+ ...meta
35326
+ }));
35327
+ await finish(ending, {
35328
+ inputTokens: 0,
35329
+ outputTokens: 0
35330
+ });
35331
+ };
35332
+ if (signal.aborted) {
35333
+ deps.logger.info("ai test chat: client had already left before the run started", withTags({}));
35334
+ return;
35335
+ }
35336
+ const profile = await deps.getProfile(request.profileId);
35337
+ if (profile === null || !profile.enabled) {
35338
+ await fail({
35339
+ code: "no-profile",
35340
+ message: `profile "${request.profileId}" not found or disabled`
35341
+ }, "ai test chat: profile missing or disabled — turn dropped");
35342
+ return;
35343
+ }
35344
+ profileForUsage = profile;
35345
+ let attachment = null;
35346
+ let images;
35347
+ if (request.attachment !== void 0) {
35348
+ if (!profile.supportsVision) {
35349
+ await fail({
35350
+ code: "bad-request",
35351
+ message: `profile "${profile.name}" is not vision-capable`
35352
+ }, "ai test chat: profile is not vision-capable — image turn refused", { model: profile.model });
35353
+ return;
35354
+ }
35355
+ emit({
35356
+ kind: "status",
35357
+ phase: "resolving-image"
35358
+ });
35359
+ const outcome = await resolveImage(deps, request.attachment);
35360
+ if (!outcome.ok) {
35361
+ await fail(outcome.failure, "ai test chat: attachment could not be resolved — vision turn refused", { attachmentKind: request.attachment.kind });
35362
+ return;
35363
+ }
35364
+ attachment = outcome.resolved.attachment;
35365
+ images = [outcome.resolved.image];
35366
+ imageCount = 1;
35367
+ }
35368
+ if (signal.aborted) {
35369
+ deps.logger.info("ai test chat: client aborted before the provider was called", withTags({}));
35370
+ return;
35371
+ }
35372
+ const streamRequest = {
35373
+ profile,
35374
+ prompt: renderTranscript(request.messages),
35375
+ ...request.system !== void 0 ? { system: request.system } : {},
35376
+ ...images !== void 0 ? { images } : {},
35377
+ ...request.maxTokens !== void 0 ? { maxTokens: request.maxTokens } : {},
35378
+ ...request.temperature !== void 0 ? { temperature: request.temperature } : {}
35379
+ };
35380
+ emit({
35381
+ kind: "status",
35382
+ phase: "connecting"
35383
+ });
35384
+ const opened = await deps.openStream(streamRequest, {
35385
+ signal,
35386
+ connectTimeoutMs: TEST_CHAT_CONNECT_TIMEOUT_MS
35387
+ });
35388
+ if (opened.kind === "connect-timeout") {
35389
+ await fail({
35390
+ code: "unavailable",
35391
+ message: `the endpoint did not accept the connection within ${String(Math.round(opened.timeoutMs / 1e3))}s — check that the provider is running and the base URL is right`
35392
+ }, "ai test chat: provider endpoint unreachable (connect timeout) — turn dropped", {
35393
+ baseUrl: profile.baseUrl ?? "",
35394
+ connectTimeoutMs: opened.timeoutMs
35395
+ });
35396
+ return;
35397
+ }
35398
+ if (opened.kind === "network") {
35399
+ await fail({
35400
+ code: "unavailable",
35401
+ message: `the endpoint could not be reached: ${opened.message}`
35402
+ }, "ai test chat: provider endpoint unreachable (network error) — turn dropped", {
35403
+ baseUrl: profile.baseUrl ?? "",
35404
+ error: opened.message
35405
+ });
35406
+ return;
35407
+ }
35408
+ const streamed = opened.kind === "open";
35409
+ emit({
35410
+ kind: "meta",
35411
+ profileName: profile.name,
35412
+ model: profile.model,
35413
+ mediaSent: attachment,
35414
+ streamed,
35415
+ firstTokenTimeoutMs: request.firstTokenTimeoutMs
35416
+ });
35417
+ const warnIfPictureLikelyMissing = (inputTokens) => {
35418
+ if (attachment === null || inputTokens <= 0) return;
35419
+ if (inputTokens >= 100) return;
35420
+ deps.logger.warn("ai test chat: image turn billed like a text turn — the picture may not have reached the model", withTags({
35421
+ profileId: profile.id,
35422
+ model: profile.model,
35423
+ inputTokens,
35424
+ expectedAtLeast: 100,
35425
+ attachmentBytes: attachment.sizeBytes
35426
+ }));
35427
+ };
35428
+ if (!streamed) {
35429
+ emit({
35430
+ kind: "status",
35431
+ phase: "first-token-wait"
35432
+ });
35433
+ const raced = await withDeadline(deps.generateOnce(streamRequest), request.firstTokenTimeoutMs);
35434
+ if (signal.aborted) {
35435
+ deps.logger.info("ai test chat: client aborted mid-generation", withTags({}));
35436
+ return;
35437
+ }
35438
+ if (raced.timedOut) {
35439
+ await failFirstToken();
35440
+ return;
35441
+ }
35442
+ const result = raced.value;
35443
+ if (!result.ok) {
35444
+ await fail({
35445
+ code: result.code,
35446
+ message: result.message
35447
+ }, "ai test chat: provider returned an error — turn dropped", {
35448
+ code: result.code,
35449
+ error: result.message
35450
+ });
35451
+ return;
35452
+ }
35453
+ warnIfPictureLikelyMissing(result.usage.inputTokens);
35454
+ emit({
35455
+ kind: "token",
35456
+ text: result.text
35457
+ });
35458
+ emit({
35459
+ kind: "done",
35460
+ inputTokens: result.usage.inputTokens,
35461
+ outputTokens: result.usage.outputTokens,
35462
+ latencyMs: deps.now() - startedAt,
35463
+ truncated: result.truncated
35464
+ });
35465
+ await finish(null, {
35466
+ inputTokens: result.usage.inputTokens,
35467
+ outputTokens: result.usage.outputTokens
35468
+ });
35469
+ return;
35470
+ }
35471
+ async function failFirstToken() {
35472
+ await fail({
35473
+ code: "timeout",
35474
+ message: `the model produced no output within ${String(Math.round(request.firstTokenTimeoutMs / 1e3))}s — a cold load can take minutes; warm the model or raise the first-token timeout`
35475
+ }, "ai test chat: no first token before the bound — model probably still loading", { firstTokenTimeoutMs: request.firstTokenTimeoutMs });
35476
+ }
35477
+ emit({
35478
+ kind: "status",
35479
+ phase: "first-token-wait"
35480
+ });
35481
+ const iterator = opened.chunks[Symbol.asyncIterator]();
35482
+ let sawFirstToken = false;
35483
+ let inputTokens = 0;
35484
+ let outputTokens = 0;
35485
+ let truncated = false;
35486
+ for (;;) {
35487
+ if (signal.aborted) {
35488
+ deps.logger.info("ai test chat: client aborted mid-stream — provider call torn down", { ...withTags({ sawFirstToken }) });
35489
+ return;
35490
+ }
35491
+ const next = await nextChunk(iterator, sawFirstToken ? TEST_CHAT_IDLE_TIMEOUT_MS : request.firstTokenTimeoutMs);
35492
+ if (next.kind === "timeout") {
35493
+ if (!sawFirstToken) {
35494
+ await failFirstToken();
35495
+ return;
35496
+ }
35497
+ await fail({
35498
+ code: "timeout",
35499
+ message: `the answer stopped mid-stream — no further output for ${String(Math.round(TEST_CHAT_IDLE_TIMEOUT_MS / 1e3))}s`
35500
+ }, "ai test chat: stream stalled after the first token — turn cut short", {
35501
+ idleTimeoutMs: TEST_CHAT_IDLE_TIMEOUT_MS,
35502
+ outputTokens
35503
+ });
35504
+ return;
35505
+ }
35506
+ if (next.kind === "end") break;
35507
+ if (next.value.kind === "token") {
35508
+ if (!sawFirstToken) {
35509
+ sawFirstToken = true;
35510
+ emit({
35511
+ kind: "status",
35512
+ phase: "streaming"
35513
+ });
35514
+ }
35515
+ emit({
35516
+ kind: "token",
35517
+ text: next.value.text
35518
+ });
35519
+ continue;
35520
+ }
35521
+ inputTokens = next.value.inputTokens;
35522
+ outputTokens = next.value.outputTokens;
35523
+ truncated = next.value.truncated;
35524
+ }
35525
+ if (signal.aborted) {
35526
+ deps.logger.info("ai test chat: client aborted before the stream finished", withTags({}));
35527
+ return;
35528
+ }
35529
+ if (!sawFirstToken) {
35530
+ await fail({
35531
+ code: "adapter-error",
35532
+ message: "the provider closed the stream without producing any output"
35533
+ }, "ai test chat: provider closed an empty stream — turn dropped", {});
35534
+ return;
35535
+ }
35536
+ warnIfPictureLikelyMissing(inputTokens);
35537
+ emit({
35538
+ kind: "done",
35539
+ inputTokens,
35540
+ outputTokens,
35541
+ latencyMs: deps.now() - startedAt,
35542
+ truncated
35543
+ });
35544
+ await finish(null, {
35545
+ inputTokens,
35546
+ outputTokens
35547
+ });
35548
+ }
35549
+ //#endregion
35550
+ //#region src/test-chat/plane.ts
35551
+ /** Bounded so a malformed or hostile body cannot buy memory. */
35552
+ var MAX_BODY_BYTES = 256 * 1024;
35553
+ async function readBody(req) {
35554
+ const chunks = [];
35555
+ let total = 0;
35556
+ for await (const chunk of req) {
35557
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
35558
+ total += buf.byteLength;
35559
+ if (total > MAX_BODY_BYTES) return null;
35560
+ chunks.push(buf);
35561
+ }
35562
+ return Buffer.concat(chunks).toString("utf8");
35563
+ }
35564
+ function sendJson(res, status, body) {
35565
+ const text = JSON.stringify(body);
35566
+ res.writeHead(status, {
35567
+ "content-type": "application/json",
35568
+ "content-length": Buffer.byteLength(text),
35569
+ "cache-control": "no-store"
35570
+ });
35571
+ res.end(text);
35572
+ }
35573
+ function createTestChatPlaneHandler(deps) {
35574
+ return async (req, res) => {
35575
+ if ((req.method ?? "GET").toUpperCase() !== "POST") {
35576
+ sendJson(res, 405, { error: "POST only" });
35577
+ return;
35578
+ }
35579
+ const raw = await readBody(req);
35580
+ if (raw === null) {
35581
+ sendJson(res, 413, { error: "request body too large" });
35582
+ return;
35583
+ }
35584
+ let parsedJson;
35585
+ try {
35586
+ parsedJson = JSON.parse(raw);
35587
+ } catch {
35588
+ sendJson(res, 400, { error: "invalid JSON body" });
35589
+ return;
35590
+ }
35591
+ const parsed = TestChatRequestSchema.safeParse(parsedJson);
35592
+ if (!parsed.success) {
35593
+ const detail = parsed.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ");
35594
+ deps.logger.warn("ai test chat: request rejected — turn never ran", { meta: { detail } });
35595
+ sendJson(res, 400, {
35596
+ error: "invalid request",
35597
+ detail
35598
+ });
35599
+ return;
35600
+ }
35601
+ const controller = new AbortController();
35602
+ res.on("close", () => controller.abort());
35603
+ res.writeHead(200, {
35604
+ "content-type": "application/x-ndjson; charset=utf-8",
35605
+ "cache-control": "no-store, no-transform",
35606
+ "content-encoding": "identity",
35607
+ "x-no-compression": "1",
35608
+ connection: "keep-alive"
35609
+ });
35610
+ res.flushHeaders();
35611
+ const emit = (event) => {
35612
+ if (res.writableEnded) return;
35613
+ res.write(encodeEvent(event));
35614
+ };
35615
+ try {
35616
+ await runTestChatStream(deps.stream, parsed.data, emit, controller.signal);
35617
+ } catch (cause) {
35618
+ const message = cause instanceof Error ? cause.message : String(cause);
35619
+ deps.logger.error("ai test chat: run threw — stream closed with an error event", { meta: { error: message } });
35620
+ emit({
35621
+ kind: "error",
35622
+ code: "adapter-error",
35623
+ message
35624
+ });
35625
+ } finally {
35626
+ if (!res.writableEnded) res.end();
35627
+ }
35628
+ };
35629
+ }
35630
+ //#endregion
32682
35631
  //#region src/runtime/crash-policy.ts
32683
35632
  var CrashPolicy = class {
32684
35633
  opts;
@@ -33074,6 +36023,8 @@ var USAGE_RETENTION_DAYS = 90;
33074
36023
  var AiAddon = class extends BaseAddon {
33075
36024
  supervisor;
33076
36025
  timers = [];
36026
+ /** Hub only — see `serveTestChatPlane`. */
36027
+ testChatPlane = null;
33077
36028
  constructor() {
33078
36029
  super({});
33079
36030
  }
@@ -33090,7 +36041,7 @@ var AiAddon = class extends BaseAddon {
33090
36041
  const settingsPort = api !== void 0 ? createApiSettingsStorePort(api) : createMemorySettingsStorePort();
33091
36042
  if (api === void 0) logger.warn("addon-ai: no ctx.api — profiles are in-memory only");
33092
36043
  const binDir = path$1.join(ctx.nodeDataDir, "bin");
33093
- const { ensureLlamaServer } = await import("./ensure-llama-server-Dmh3cZ41.mjs").then((n) => n.r);
36044
+ const { ensureLlamaServer } = await import("./ensure-llama-server-BIz14RoW.mjs").then((n) => n.r);
33094
36045
  const assembly = await assembleAi({
33095
36046
  nodeId: ownNodeId,
33096
36047
  isHub,
@@ -33102,16 +36053,21 @@ var AiAddon = class extends BaseAddon {
33102
36053
  binDir,
33103
36054
  logger: logger.child("llama-bin")
33104
36055
  }),
33105
- execute: (await import("./execute-plan-KbxnOqY1.mjs").then((n) => n.n)).createFetchExecutor(),
36056
+ execute: (await import("./execute-plan-EMmlVCr5.mjs")).createFetchExecutor(),
33106
36057
  ...isHub && api !== void 0 ? { runtimeApi: this.asRuntimeApi(api) } : {},
33107
36058
  ...isHub && api !== void 0 ? { distributeModel: this.makeDistributeModel(api) } : {}
33108
36059
  });
33109
- if (isHub) this.schedulePostBoot(assembly);
36060
+ if (isHub) {
36061
+ this.schedulePostBoot(assembly);
36062
+ await this.serveTestChatPlane(assembly, api);
36063
+ }
33110
36064
  return assembly.registrations;
33111
36065
  }
33112
36066
  async onShutdown() {
33113
36067
  for (const timer of this.timers) clearTimeout(timer);
33114
36068
  this.timers.length = 0;
36069
+ await this.testChatPlane?.dispose();
36070
+ this.testChatPlane = null;
33115
36071
  await this.supervisor?.stop();
33116
36072
  }
33117
36073
  asRuntimeApi(api) {
@@ -33130,6 +36086,78 @@ var AiAddon = class extends BaseAddon {
33130
36086
  });
33131
36087
  };
33132
36088
  }
36089
+ /**
36090
+ * `POST /addon/ai/test-chat` — the admin AI page's streaming test chat.
36091
+ *
36092
+ * A DATA-PLANE route, not a cap method and not a custom action. It shipped
36093
+ * first as `addons.custom` and the operator's first real use failed with
36094
+ * `UDS request timed out after 60000ms`: `callAddonOnChild` passes no options
36095
+ * and therefore takes `DEFAULT_UDS_REQUEST_TIMEOUT_MS`, with no per-call
36096
+ * override on that path. A generation that legitimately takes minutes on a
36097
+ * cold GPU load cannot live inside a unary RPC — and the error it produced
36098
+ * named the transport rather than the model or the endpoint.
36099
+ *
36100
+ * HUB ONLY, and that is not a shortcut: the `llm` provider, the profile store
36101
+ * and the api keys exist only here, and the facility binds `127.0.0.1`, which
36102
+ * the hub could not reach on an agent anyway.
36103
+ *
36104
+ * `getProfile` reads the store DIRECTLY rather than through `listProfiles`,
36105
+ * which redacts the api key — the streamed call needs the real one, and it
36106
+ * never leaves this process either way.
36107
+ *
36108
+ * `getSnapshot` deliberately omits `force`. That flag is an OPERATOR signal
36109
+ * that walks past the wrapper's battery-camera sleep gate, and a chat turn is
36110
+ * not a reason to wake a camera: a stale-but-honest frame is the right answer
36111
+ * and the `meta` event reports how old it is.
36112
+ */
36113
+ async serveTestChatPlane(assembly, api) {
36114
+ const store = assembly.store;
36115
+ const provider = assembly.llmProvider;
36116
+ const usage = assembly.usage;
36117
+ if (store === void 0 || provider === void 0 || usage === void 0 || api === void 0) {
36118
+ this.ctx.logger.warn("addon-ai: test-chat plane not served — no llm provider on this node");
36119
+ return;
36120
+ }
36121
+ const logger = this.ctx.logger.child("llm-test-chat");
36122
+ const stream = {
36123
+ getProfile: (profileId) => store.getById(profileId),
36124
+ openStream: createChatCompletionsStreamer(),
36125
+ generateOnce: async (request) => {
36126
+ const base = {
36127
+ profileId: request.profile.id,
36128
+ consumer: TEST_CHAT_CONSUMER,
36129
+ prompt: request.prompt,
36130
+ ...request.system !== void 0 ? { system: request.system } : {},
36131
+ ...request.maxTokens !== void 0 ? { maxTokens: request.maxTokens } : {},
36132
+ ...request.temperature !== void 0 ? { temperature: request.temperature } : {}
36133
+ };
36134
+ return request.images === void 0 || request.images.length === 0 ? provider.generate(base) : provider.generateVision({
36135
+ ...base,
36136
+ images: [...request.images]
36137
+ });
36138
+ },
36139
+ getSnapshot: (deviceId) => api.snapshot.getSnapshot.query({ deviceId }),
36140
+ getTrackMedia: (trackId, kinds) => api.pipelineAnalytics.getTrackMedia.query({
36141
+ trackId,
36142
+ kinds: [...kinds]
36143
+ }),
36144
+ recordUsage: (row) => usage.record(row),
36145
+ logger,
36146
+ now: () => Date.now()
36147
+ };
36148
+ this.testChatPlane = await this.ctx.dataPlane?.serve({
36149
+ prefix: "test-chat",
36150
+ access: "authenticated",
36151
+ handler: createTestChatPlaneHandler({
36152
+ stream,
36153
+ logger
36154
+ })
36155
+ }) ?? null;
36156
+ this.ctx.logger.info("ai test-chat data-plane served", { meta: {
36157
+ served: this.testChatPlane !== null,
36158
+ path: `/addon/${this.ctx.id}/${TEST_CHAT_PREFIX}`
36159
+ } });
36160
+ }
33133
36161
  schedulePostBoot(assembly) {
33134
36162
  const kick = setTimeout(() => {
33135
36163
  assembly.prune?.(USAGE_RETENTION_DAYS).catch(() => void 0);
@@ -33162,4 +36190,4 @@ var AiAddon = class extends BaseAddon {
33162
36190
  }
33163
36191
  };
33164
36192
  //#endregion
33165
- export { object as A, AiAddon, AiAddon as default, googleAdapter as C, LlmProfileKindSchema as D, LlmErrorCodeSchema as E, boolean as O, openAiAdapter as S, openAiCompatibleAdapter as T, redactProfileForRead as _, createLlmRuntimeProvider as a, SEED_PROFILE_ID as b, LLM_MODEL_CATALOG as c, createRuntimeClient as d, AI_ADDON_ID as f, mergeProfileSecrets as g, REDACTED_MARKER as h, assembleAi as i, string as j, number as k, catalogById as l, resolveProfile as m, createMemorySettingsStorePort as n, fileSha256 as o, createLlmProvider as p, LlamaSupervisor as r, createDefaultModelOps as s, createApiSettingsStorePort as t, entryForRef as u, PROFILES_COLLECTION as v, anthropicAdapter as w, DefaultsStore as x, ProfileStore as y };
36193
+ export { AI_ADDON_ID as A, AiAddon, AiAddon as default, createLlmRuntimeProvider as C, catalogById as D, LLM_MODEL_CATALOG as E, number as F, object as I, string as L, LlmErrorCodeSchema as M, LlmProfileKindSchema as N, entryForRef as O, boolean as P, sseData as S, createDefaultModelOps as T, TestChatEventSchema as _, runTestChatStream as a, chunksFromSse as b, renderTranscript as c, TEST_CHAT_CONSUMER as d, TEST_CHAT_DEFAULT_FIRST_TOKEN_TIMEOUT_MS as f, TEST_CHAT_PREFIX as g, TEST_CHAT_MIN_VISION_INPUT_TOKENS as h, createTestChatPlaneHandler as i, createLlmProvider as j, createRuntimeClient as k, resolveImage as l, TEST_CHAT_MAX_FIRST_TOKEN_TIMEOUT_MS as m, createMemorySettingsStorePort as n, TRACK_MEDIA_PREFERENCE as o, TEST_CHAT_IDLE_TIMEOUT_MS as p, LlamaSupervisor as r, pickTrackMedia as s, createApiSettingsStorePort as t, TEST_CHAT_CONNECT_TIMEOUT_MS as u, TestChatRequestSchema as v, fileSha256 as w, createChatCompletionsStreamer as x, encodeEvent as y };