@camstack/addon-ai 0.2.21 → 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",
@@ -14083,6 +14130,8 @@ var NcSystemEventKindSchema = _enum([
14083
14130
  "stream-offline",
14084
14131
  "node-online",
14085
14132
  "node-offline",
14133
+ "node-inference-unavailable",
14134
+ "detection-blind",
14086
14135
  "addon-update-available",
14087
14136
  "server-update-available",
14088
14137
  "alarm-triggered",
@@ -14144,7 +14193,16 @@ var NcScheduleSchema = object({
14144
14193
  });
14145
14194
  /** Fuzzy plate matcher — OCR noise makes exact match useless (spec row 12/13). */
14146
14195
  var NcPlateMatcherSchema = object({
14147
- 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)),
14148
14206
  /** Max Levenshtein distance after normalization (uppercase alphanumeric). */
14149
14207
  maxDistance: number().int().min(0).max(3).default(1)
14150
14208
  });
@@ -14383,18 +14441,47 @@ var NcConditionsSchema = object({
14383
14441
  */
14384
14442
  labelEquals: array(string().min(1)).optional(),
14385
14443
  /**
14386
- * Identity matcher. P1 boundary: matched against the record's collapsed
14387
- * `label` (the identity display name propagated by the face pipeline) —
14388
- * 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.
14389
14472
  */
14390
14473
  identities: array(string().min(1)).optional(),
14391
- /** 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
+ */
14392
14479
  plates: NcPlateMatcherSchema.optional(),
14393
14480
  /**
14394
- * Identity EXCLUDE — mirror of {@link identities} with `notIn` semantics.
14395
- * Same P1 boundary: matched against the record's collapsed `label` (the
14396
- * identity display name). A record with NO label passes (nothing to
14397
- * 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.
14398
14485
  */
14399
14486
  identitiesExclude: array(string().min(1)).optional(),
14400
14487
  /**
@@ -14786,7 +14873,80 @@ var NcRuleInputSchema = object({
14786
14873
  * a rule that predates the gate must keep delivering byte-for-byte as it
14787
14874
  * did, and absent is the only way to say that without a migration.
14788
14875
  */
14789
- 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()
14790
14950
  });
14791
14951
  /**
14792
14952
  * Partial patch for `updateRule` — any subset of the input fields, plus the
@@ -15298,7 +15458,87 @@ var MethodAccessSchema = _enum([
15298
15458
  var AllowedProviderSchema = union([literal("*"), array(string())]);
15299
15459
  var AllowedDevicesSchema = record(string(), union([literal("*"), array(string())]));
15300
15460
  var CapScopeSchema = _enum(["device", "system"]);
15301
- 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", [
15302
15542
  object({
15303
15543
  type: literal("category"),
15304
15544
  target: CapScopeSchema,
@@ -15314,18 +15554,8 @@ var TokenScopeSchema = discriminatedUnion("type", [
15314
15554
  target: string(),
15315
15555
  access: array(MethodAccessSchema).min(1)
15316
15556
  }),
15317
- object({
15318
- type: literal("device"),
15319
- /**
15320
- * One or more deviceIds (serialised as strings for wire-format
15321
- * consistency with the rest of the union). Matcher accepts if
15322
- * `input.deviceId` ∈ `targets`. Array shape avoids the row-explosion
15323
- * of one scope-per-device when granting access to a set of cameras.
15324
- */
15325
- targets: array(string()).min(1),
15326
- access: array(MethodAccessSchema).min(1)
15327
- })
15328
- ]);
15557
+ DeviceTokenScopeSchema
15558
+ ]));
15329
15559
  object({
15330
15560
  id: string(),
15331
15561
  username: string(),
@@ -15642,7 +15872,7 @@ var TrackEnvelopeSchema = object({
15642
15872
  * `snapshots[]` references — megabytes across a page of tracks. `slim`
15643
15873
  * keeps every scalar the list surfaces actually render (ids, class(es),
15644
15874
  * label / audioLabels / importance enrichment, firstSeen/lastSeen, state,
15645
- * zonesVisited, bestEventId, envelope, hasFace) and returns `positions` /
15875
+ * zonesVisited, bestEventId, envelope, hasFace, hasRider) and returns `positions` /
15646
15876
  * `snapshots` as EMPTY arrays — detail views re-fetch the full row via
15647
15877
  * `getTrack`. Mirrors the event-store `projection` convention
15648
15878
  * (`getObjectEvents` et al.).
@@ -15778,7 +16008,21 @@ union([literal(1), literal(2)]);
15778
16008
  var LabelAttributionSchema = object({
15779
16009
  stepId: string(),
15780
16010
  modelId: string().optional(),
15781
- 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()
15782
16026
  });
15783
16027
  /**
15784
16028
  * The TIERED label model (roadmap 4g), spread into `TrackSchema` and
@@ -15915,6 +16159,28 @@ var TrackSchema = object({
15915
16159
  * `=== true` and render nothing otherwise, never infer "no face".
15916
16160
  */
15917
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(),
15918
16184
  ...TrackFlagFields,
15919
16185
  ...TrackRetrainFields
15920
16186
  });
@@ -17341,7 +17607,7 @@ var detectionFpsField = {
17341
17607
  var occupancyRecheckSecField = {
17342
17608
  min: 0,
17343
17609
  max: 300,
17344
- default: 30,
17610
+ default: 300,
17345
17611
  step: 5
17346
17612
  };
17347
17613
  var occupancyRecheckFramesField = {
@@ -23656,7 +23922,7 @@ method(object({
23656
23922
  toMs: number()
23657
23923
  }), RecordingAvailabilitySchema, {
23658
23924
  kind: "query",
23659
- auth: "admin"
23925
+ auth: "protected"
23660
23926
  }), method(object({
23661
23927
  deviceId: number(),
23662
23928
  fromMs: number(),
@@ -23664,14 +23930,14 @@ method(object({
23664
23930
  tzOffsetMinutes: number()
23665
23931
  }), RecordingDaysSchema, {
23666
23932
  kind: "query",
23667
- auth: "admin"
23933
+ auth: "protected"
23668
23934
  }), method(object({
23669
23935
  deviceId: number(),
23670
23936
  fromMs: number(),
23671
23937
  toMs: number()
23672
23938
  }), RecordingManifestSchema, {
23673
23939
  kind: "query",
23674
- auth: "admin"
23940
+ auth: "protected"
23675
23941
  }), method(object({}), RecordingStorageUsageSchema, {
23676
23942
  kind: "query",
23677
23943
  auth: "admin"
@@ -23966,9 +24232,33 @@ method(object({
23966
24232
  * settings-contribution methods. `status.kind:'push'` — the engine pushes on
23967
24233
  * every hysteresis flip / availability change; consumers never poll.
23968
24234
  */
23969
- /** Extensible condition tag. Seeded 'day' | 'night'; open by design so more can
23970
- * 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. */
23971
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
+ ]);
23972
24262
  /** One captured reference — condition-tagged, model-version-gated. `embedding`
23973
24263
  * is `number[]` (Float32Array does NOT survive MsgPack/UDS). */
23974
24264
  var SceneReferenceSchema = object({
@@ -23976,7 +24266,14 @@ var SceneReferenceSchema = object({
23976
24266
  modelId: string(),
23977
24267
  condition: SceneConditionSchema,
23978
24268
  capturedAt: number(),
23979
- 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()
23980
24277
  });
23981
24278
  var SceneMonitorStateSchema = object({
23982
24279
  id: string(),
@@ -23998,6 +24295,25 @@ var SceneCheckSchema = discriminatedUnion("mode", [object({
23998
24295
  profileId: string().optional(),
23999
24296
  hysteresisCount: number().int().positive()
24000
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
+ });
24001
24317
  var SceneMonitorSchema = object({
24002
24318
  id: string(),
24003
24319
  label: string(),
@@ -24016,7 +24332,41 @@ var SceneMonitorSchema = object({
24016
24332
  lastConfidence: number().nullable(),
24017
24333
  currentCondition: SceneConditionSchema.nullable(),
24018
24334
  availability: _enum(["ok", "unavailable"]),
24019
- 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)
24020
24370
  });
24021
24371
  var SceneMonitorStatusSchema = object({
24022
24372
  monitors: array(SceneMonitorSchema),
@@ -24049,7 +24399,14 @@ DeviceType.Camera, method(object({ deviceId: number() }), SceneMonitorStatusSche
24049
24399
  "both"
24050
24400
  ]).optional(),
24051
24401
  checkIntervalSec: number().optional(),
24052
- 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()
24053
24410
  })
24054
24411
  }), _void(), {
24055
24412
  kind: "mutation",
@@ -24086,6 +24443,14 @@ DeviceType.Camera, method(object({ deviceId: number() }), SceneMonitorStatusSche
24086
24443
  }), _void(), {
24087
24444
  kind: "mutation",
24088
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"
24089
24454
  });
24090
24455
  /**
24091
24456
  * Per-stage gating mode applied to the zones a rule references.
@@ -29379,6 +29744,12 @@ Object.freeze({
29379
29744
  addonId: null,
29380
29745
  access: "create"
29381
29746
  },
29747
+ "sceneMonitor.resetScene": {
29748
+ capName: "scene-monitor",
29749
+ capScope: "device",
29750
+ addonId: null,
29751
+ access: "delete"
29752
+ },
29382
29753
  "sceneMonitor.updateScene": {
29383
29754
  capName: "scene-monitor",
29384
29755
  capScope: "device",
@@ -30670,6 +31041,1683 @@ Object.freeze({
30670
31041
  access: "create"
30671
31042
  }
30672
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
+ });
30673
32721
  Object.freeze({
30674
32722
  "broker": "broker",
30675
32723
  "device-export": "device-export",
@@ -31097,7 +33145,7 @@ function mapHttpError(res) {
31097
33145
  };
31098
33146
  }
31099
33147
  /** Narrow an unknown to a plain object record (documented JSON type boundary). */
31100
- function asRecord(value) {
33148
+ function asRecord$1(value) {
31101
33149
  if (value !== null && typeof value === "object" && !Array.isArray(value)) return value;
31102
33150
  return null;
31103
33151
  }
@@ -31224,10 +33272,10 @@ function buildChatCompletionsBody(profile, request) {
31224
33272
  };
31225
33273
  return body;
31226
33274
  }
31227
- function readString$2(value) {
33275
+ function readString$3(value) {
31228
33276
  return typeof value === "string" ? value : void 0;
31229
33277
  }
31230
- function readNumber$2(value) {
33278
+ function readNumber$3(value) {
31231
33279
  return typeof value === "number" && Number.isFinite(value) ? value : 0;
31232
33280
  }
31233
33281
  function parseChatCompletionsResult(res, latencyMs) {
@@ -31239,33 +33287,33 @@ function parseChatCompletionsResult(res, latencyMs) {
31239
33287
  code: "adapter-error",
31240
33288
  message: "invalid JSON response"
31241
33289
  };
31242
- const choice = asRecord((Array.isArray(body.choices) ? body.choices : [])[0]);
33290
+ const choice = asRecord$1((Array.isArray(body.choices) ? body.choices : [])[0]);
31243
33291
  if (choice === null) return {
31244
33292
  ok: false,
31245
33293
  code: "adapter-error",
31246
33294
  message: "missing choices[0]"
31247
33295
  };
31248
- const finishReason = readString$2(choice.finish_reason);
33296
+ const finishReason = readString$3(choice.finish_reason);
31249
33297
  if (finishReason === "content_filter") return {
31250
33298
  ok: false,
31251
33299
  code: "refusal",
31252
33300
  message: "content filtered"
31253
33301
  };
31254
- const messageObj = asRecord(choice.message);
31255
- 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;
31256
33304
  if (text === void 0) return {
31257
33305
  ok: false,
31258
33306
  code: "adapter-error",
31259
33307
  message: "missing message.content"
31260
33308
  };
31261
- const usageObj = asRecord(body.usage) ?? {};
33309
+ const usageObj = asRecord$1(body.usage) ?? {};
31262
33310
  return {
31263
33311
  ok: true,
31264
33312
  text,
31265
- model: readString$2(body.model) ?? "",
33313
+ model: readString$3(body.model) ?? "",
31266
33314
  usage: {
31267
- inputTokens: readNumber$2(usageObj.prompt_tokens),
31268
- outputTokens: readNumber$2(usageObj.completion_tokens)
33315
+ inputTokens: readNumber$3(usageObj.prompt_tokens),
33316
+ outputTokens: readNumber$3(usageObj.completion_tokens)
31269
33317
  },
31270
33318
  truncated: finishReason === "length",
31271
33319
  latencyMs
@@ -31276,7 +33324,7 @@ function parseModelsList(res) {
31276
33324
  const data = body !== null && Array.isArray(body.data) ? body.data : [];
31277
33325
  const ids = [];
31278
33326
  for (const entry of data) if (entry !== null && typeof entry === "object") {
31279
- const id = readString$2(entry.id);
33327
+ const id = readString$3(entry.id);
31280
33328
  if (id !== void 0) ids.push(id);
31281
33329
  }
31282
33330
  return ids;
@@ -31342,10 +33390,10 @@ function userContent(request) {
31342
33390
  });
31343
33391
  return parts;
31344
33392
  }
31345
- function readString$1(value) {
33393
+ function readString$2(value) {
31346
33394
  return typeof value === "string" ? value : void 0;
31347
33395
  }
31348
- function readNumber$1(value) {
33396
+ function readNumber$2(value) {
31349
33397
  return typeof value === "number" && Number.isFinite(value) ? value : 0;
31350
33398
  }
31351
33399
  function buildMessagesBody(profile, request) {
@@ -31382,7 +33430,7 @@ function parseMessagesResult(res, latencyMs) {
31382
33430
  code: "adapter-error",
31383
33431
  message: "invalid JSON response"
31384
33432
  };
31385
- const stopReason = readString$1(body.stop_reason);
33433
+ const stopReason = readString$2(body.stop_reason);
31386
33434
  if (stopReason === "refusal") return {
31387
33435
  ok: false,
31388
33436
  code: "refusal",
@@ -31397,7 +33445,7 @@ function parseMessagesResult(res, latencyMs) {
31397
33445
  text = JSON.stringify(b.input);
31398
33446
  break;
31399
33447
  }
31400
- 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);
31401
33449
  }
31402
33450
  if (text === void 0) return {
31403
33451
  ok: false,
@@ -31408,10 +33456,10 @@ function parseMessagesResult(res, latencyMs) {
31408
33456
  return {
31409
33457
  ok: true,
31410
33458
  text,
31411
- model: readString$1(body.model) ?? "",
33459
+ model: readString$2(body.model) ?? "",
31412
33460
  usage: {
31413
- inputTokens: readNumber$1(usageObj.input_tokens),
31414
- outputTokens: readNumber$1(usageObj.output_tokens)
33461
+ inputTokens: readNumber$2(usageObj.input_tokens),
33462
+ outputTokens: readNumber$2(usageObj.output_tokens)
31415
33463
  },
31416
33464
  truncated: stopReason === "max_tokens",
31417
33465
  latencyMs
@@ -31446,7 +33494,7 @@ var anthropicAdapter = {
31446
33494
  const data = body !== null && Array.isArray(body.data) ? body.data : [];
31447
33495
  const ids = [];
31448
33496
  for (const entry of data) if (entry !== null && typeof entry === "object") {
31449
- const id = readString$1(entry.id);
33497
+ const id = readString$2(entry.id);
31450
33498
  if (id !== void 0) ids.push(id);
31451
33499
  }
31452
33500
  return ids;
@@ -31464,10 +33512,10 @@ function headers(profile) {
31464
33512
  ...profile.extraHeaders
31465
33513
  };
31466
33514
  }
31467
- function readString(value) {
33515
+ function readString$1(value) {
31468
33516
  return typeof value === "string" ? value : void 0;
31469
33517
  }
31470
- function readNumber(value) {
33518
+ function readNumber$1(value) {
31471
33519
  return typeof value === "number" && Number.isFinite(value) ? value : 0;
31472
33520
  }
31473
33521
  function buildGenerateContentBody(profile, request) {
@@ -31502,37 +33550,37 @@ function parseGenerateContentResult(res, latencyMs) {
31502
33550
  code: "adapter-error",
31503
33551
  message: "invalid JSON response"
31504
33552
  };
31505
- const promptFeedback = asRecord(body.promptFeedback);
31506
- 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 {
31507
33555
  ok: false,
31508
33556
  code: "refusal",
31509
- message: `blocked: ${readString(promptFeedback.blockReason)}`
33557
+ message: `blocked: ${readString$1(promptFeedback.blockReason)}`
31510
33558
  };
31511
- const candidate = asRecord((Array.isArray(body.candidates) ? body.candidates : [])[0]);
33559
+ const candidate = asRecord$1((Array.isArray(body.candidates) ? body.candidates : [])[0]);
31512
33560
  if (candidate === null) return {
31513
33561
  ok: false,
31514
33562
  code: "adapter-error",
31515
33563
  message: "missing candidates[0]"
31516
33564
  };
31517
- const finishReason = readString(candidate.finishReason);
33565
+ const finishReason = readString$1(candidate.finishReason);
31518
33566
  if (finishReason === "SAFETY" || finishReason === "PROHIBITED_CONTENT") return {
31519
33567
  ok: false,
31520
33568
  code: "refusal",
31521
33569
  message: `finish: ${finishReason}`
31522
33570
  };
31523
- const contentObj = asRecord(candidate.content) ?? {};
33571
+ const contentObj = asRecord$1(candidate.content) ?? {};
31524
33572
  const text = (Array.isArray(contentObj.parts) ? contentObj.parts : []).map((p) => {
31525
- const rec = asRecord(p);
31526
- return rec !== null ? readString(rec.text) : void 0;
33573
+ const rec = asRecord$1(p);
33574
+ return rec !== null ? readString$1(rec.text) : void 0;
31527
33575
  }).filter((t) => t !== void 0).join("");
31528
- const usageObj = asRecord(body.usageMetadata) ?? {};
33576
+ const usageObj = asRecord$1(body.usageMetadata) ?? {};
31529
33577
  return {
31530
33578
  ok: true,
31531
33579
  text,
31532
- model: readString(body.modelVersion) ?? "",
33580
+ model: readString$1(body.modelVersion) ?? "",
31533
33581
  usage: {
31534
- inputTokens: readNumber(usageObj.promptTokenCount),
31535
- outputTokens: readNumber(usageObj.candidatesTokenCount)
33582
+ inputTokens: readNumber$1(usageObj.promptTokenCount),
33583
+ outputTokens: readNumber$1(usageObj.candidatesTokenCount)
31536
33584
  },
31537
33585
  truncated: finishReason === "MAX_TOKENS",
31538
33586
  latencyMs
@@ -31570,7 +33618,7 @@ var googleAdapter = {
31570
33618
  const models = body !== null && Array.isArray(body.models) ? body.models : [];
31571
33619
  const ids = [];
31572
33620
  for (const entry of models) if (entry !== null && typeof entry === "object") {
31573
- const name = readString(entry.name);
33621
+ const name = readString$1(entry.name);
31574
33622
  if (name !== void 0) ids.push(stripModelsPrefix(name));
31575
33623
  }
31576
33624
  return ids;
@@ -32710,7 +34758,7 @@ async function assembleAi(deps) {
32710
34758
  registrations,
32711
34759
  runtimeProvider
32712
34760
  };
32713
- 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);
32714
34762
  const store = new ProfileStore(deps.settingsPort);
32715
34763
  const defaults = new DefaultsStore(deps.settingsPort);
32716
34764
  const usage = new UsageStore(deps.settingsPort, Date.now, deps.logger.child("llm-usage"));
@@ -32743,10 +34791,843 @@ async function assembleAi(deps) {
32743
34791
  runtimeProvider,
32744
34792
  llmProvider,
32745
34793
  store,
34794
+ usage,
32746
34795
  prune: (retentionDays) => usage.prune(retentionDays)
32747
34796
  };
32748
34797
  }
32749
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
32750
35631
  //#region src/runtime/crash-policy.ts
32751
35632
  var CrashPolicy = class {
32752
35633
  opts;
@@ -33142,6 +36023,8 @@ var USAGE_RETENTION_DAYS = 90;
33142
36023
  var AiAddon = class extends BaseAddon {
33143
36024
  supervisor;
33144
36025
  timers = [];
36026
+ /** Hub only — see `serveTestChatPlane`. */
36027
+ testChatPlane = null;
33145
36028
  constructor() {
33146
36029
  super({});
33147
36030
  }
@@ -33158,7 +36041,7 @@ var AiAddon = class extends BaseAddon {
33158
36041
  const settingsPort = api !== void 0 ? createApiSettingsStorePort(api) : createMemorySettingsStorePort();
33159
36042
  if (api === void 0) logger.warn("addon-ai: no ctx.api — profiles are in-memory only");
33160
36043
  const binDir = path$1.join(ctx.nodeDataDir, "bin");
33161
- 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);
33162
36045
  const assembly = await assembleAi({
33163
36046
  nodeId: ownNodeId,
33164
36047
  isHub,
@@ -33170,16 +36053,21 @@ var AiAddon = class extends BaseAddon {
33170
36053
  binDir,
33171
36054
  logger: logger.child("llama-bin")
33172
36055
  }),
33173
- execute: (await import("./execute-plan-KbxnOqY1.mjs").then((n) => n.n)).createFetchExecutor(),
36056
+ execute: (await import("./execute-plan-EMmlVCr5.mjs")).createFetchExecutor(),
33174
36057
  ...isHub && api !== void 0 ? { runtimeApi: this.asRuntimeApi(api) } : {},
33175
36058
  ...isHub && api !== void 0 ? { distributeModel: this.makeDistributeModel(api) } : {}
33176
36059
  });
33177
- if (isHub) this.schedulePostBoot(assembly);
36060
+ if (isHub) {
36061
+ this.schedulePostBoot(assembly);
36062
+ await this.serveTestChatPlane(assembly, api);
36063
+ }
33178
36064
  return assembly.registrations;
33179
36065
  }
33180
36066
  async onShutdown() {
33181
36067
  for (const timer of this.timers) clearTimeout(timer);
33182
36068
  this.timers.length = 0;
36069
+ await this.testChatPlane?.dispose();
36070
+ this.testChatPlane = null;
33183
36071
  await this.supervisor?.stop();
33184
36072
  }
33185
36073
  asRuntimeApi(api) {
@@ -33198,6 +36086,78 @@ var AiAddon = class extends BaseAddon {
33198
36086
  });
33199
36087
  };
33200
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
+ }
33201
36161
  schedulePostBoot(assembly) {
33202
36162
  const kick = setTimeout(() => {
33203
36163
  assembly.prune?.(USAGE_RETENTION_DAYS).catch(() => void 0);
@@ -33230,4 +36190,4 @@ var AiAddon = class extends BaseAddon {
33230
36190
  }
33231
36191
  };
33232
36192
  //#endregion
33233
- 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 };