@trillboards/edge-sdk 0.2.134 → 0.2.135
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/deploy/macos/scripts/setup-litertlm.sh +101 -22
- package/deploy/macos/test-setup-litertlm-reinstall.sh +91 -7
- package/deploy/systemd/setup-litertlm.sh +110 -20
- package/deploy/windows/setup-litertlm.ps1 +63 -10
- package/dist/EdgeAgent.js +1 -1
- package/dist/diagnose.d.ts +1 -1
- package/dist/diagnose.js +1 -1
- package/package.json +1 -1
|
@@ -125,20 +125,39 @@ CONFIG_JSON="$LITERT_DIR/config.json"
|
|
|
125
125
|
LITERTLM_PROBE_JPEG_B64="/9j/4AAQSkZJRgABAgAAAQABAAD//gAPTGF2YzYzLjEuMTAxAP/bAEMACD4+ST5JVVVVVVVVZF1kaGhoZGRkZGhoaHBwcIODg3BwcGhocHB8fIODj5OPh4eDh5OTm5uburqystnZ4P/////EAEoAAQAAAAAAAAAAAAAAAAAAAAABAQAAAAAAAAAAAAAAAAAAAAAQAQAAAAAAAAAAAAAAAAAAAAARAQAAAAAAAAAAAAAAAAAAAAD/wAARCAAgACADASIAAhEAAxEA/9oADAMBAAIRAxEAPwAAAAAAAAD/2Q=="
|
|
126
126
|
|
|
127
127
|
# Write config.json. $1 = "gpu" pins the vision encoder; empty leaves it on the
|
|
128
|
-
# CPU default.
|
|
129
|
-
#
|
|
128
|
+
# CPU default. $2 = "true"/"false" for speculative decoding.
|
|
129
|
+
#
|
|
130
|
+
# BOTH ARE ALWAYS WRITTEN EXPLICITLY, AND THE SECOND ONE IS NEW.
|
|
131
|
+
#
|
|
132
|
+
# `speculative_decoding` used to be written unconditionally `true` under a
|
|
133
|
+
# comment asserting it is "delegate independent, so it survives the fallback".
|
|
134
|
+
# The evidence for that is two arms on ONE machine — an M4 Max, GPU/Metal for the
|
|
135
|
+
# headline 1.433x, and its own CPU arm for the 2026-08-19 A/B in
|
|
136
|
+
# `cctv-local-inference-hardware-floor.md`. Neither is a measurement of any box
|
|
137
|
+
# we sell, and this file already refuses to leave an unverified `vision_backend`
|
|
138
|
+
# on disk for exactly that reason. The rung below applies the same rule to this
|
|
139
|
+
# key: prove it on THIS box, and back it out if the serve cannot answer with it.
|
|
140
|
+
#
|
|
141
|
+
# EXPLICIT `false` RATHER THAN OMITTING THE KEY, and that is measured too:
|
|
142
|
+
# litert-lm 0.16.0 logs `Using speculative_decoding from config for model
|
|
143
|
+
# '<id>': <bool>` on every request when the key is PRESENT and says NOTHING AT
|
|
144
|
+
# ALL when it is absent (checked on a c4-standard-8, 2026-09-04). A box whose
|
|
145
|
+
# state is unreadable from its own log is a box whose next incident is diagnosed
|
|
146
|
+
# by guessing — which is what happened when a GCE box SEGV-looped on 2026-09-02
|
|
147
|
+
# and the config had to be edited by hand to find out what it had been doing.
|
|
130
148
|
write_litertlm_config() {
|
|
131
149
|
_vision="$1"
|
|
150
|
+
_speculative="${2:-true}"
|
|
132
151
|
mkdir -p "$LITERT_DIR" 2>/dev/null || true
|
|
133
152
|
if [ -n "$_vision" ]; then
|
|
134
|
-
printf '{"models":{"%s":{"vision_backend":"%s","speculative_decoding"
|
|
135
|
-
"$MODEL_ID" "$_vision" > "$CONFIG_JSON.tmp" 2>/dev/null || return 1
|
|
153
|
+
printf '{"models":{"%s":{"vision_backend":"%s","speculative_decoding":%s}}}\n' \
|
|
154
|
+
"$MODEL_ID" "$_vision" "$_speculative" > "$CONFIG_JSON.tmp" 2>/dev/null || return 1
|
|
136
155
|
else
|
|
137
|
-
printf '{"models":{"%s":{"speculative_decoding"
|
|
138
|
-
"$MODEL_ID" > "$CONFIG_JSON.tmp" 2>/dev/null || return 1
|
|
156
|
+
printf '{"models":{"%s":{"speculative_decoding":%s}}}\n' \
|
|
157
|
+
"$MODEL_ID" "$_speculative" > "$CONFIG_JSON.tmp" 2>/dev/null || return 1
|
|
139
158
|
fi
|
|
140
159
|
mv -f "$CONFIG_JSON.tmp" "$CONFIG_JSON" 2>/dev/null || return 1
|
|
141
|
-
log "wrote $CONFIG_JSON (vision_backend=${_vision:-cpu-default}, speculative_decoding
|
|
160
|
+
log "wrote $CONFIG_JSON (vision_backend=${_vision:-cpu-default}, speculative_decoding=$_speculative)"
|
|
142
161
|
}
|
|
143
162
|
|
|
144
163
|
# -- The one knob config.json cannot express -----------------------------------
|
|
@@ -237,6 +256,26 @@ verify_litertlm_vision() {
|
|
|
237
256
|
[ "$_code" = "200" ]
|
|
238
257
|
}
|
|
239
258
|
|
|
259
|
+
# The same probe, but asking for NO delegate suffix rather than `,gpu`.
|
|
260
|
+
#
|
|
261
|
+
# WHY A SECOND FUNCTION AND NOT A FLAG: `verify_litertlm_vision` posts
|
|
262
|
+
# `$MODEL_ID,gpu`, which is what the connector sends, and litert-lm returns 200
|
|
263
|
+
# for a delegate token it cannot honour — so on the later rungs that suffix would
|
|
264
|
+
# prove nothing about the thing being tested and could mask it. Here the model id
|
|
265
|
+
# carries no suffix at all, so a 200 means the box built an engine and ran a real
|
|
266
|
+
# image through it under the config now on disk, which is exactly the claim each
|
|
267
|
+
# rung needs.
|
|
268
|
+
verify_litertlm_plain_inference() {
|
|
269
|
+
_body="$LITERT_DIR/.plain-probe.json"
|
|
270
|
+
printf '{"model":"%s","messages":[{"role":"user","content":[{"type":"text","text":"Reply with the single word: ok"},{"type":"image_url","image_url":{"url":"data:image/jpeg;base64,%s"}}]}],"max_completion_tokens":8,"temperature":0}' \
|
|
271
|
+
"$MODEL_ID" "$LITERTLM_PROBE_JPEG_B64" > "$_body" 2>/dev/null || return 1
|
|
272
|
+
_code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 180 \
|
|
273
|
+
-X POST "http://127.0.0.1:$SERVE_PORT/v1/chat/completions" \
|
|
274
|
+
-H 'Content-Type: application/json' --data-binary @"$_body" 2>/dev/null)
|
|
275
|
+
rm -f "$_body" 2>/dev/null || true
|
|
276
|
+
[ "$_code" = "200" ]
|
|
277
|
+
}
|
|
278
|
+
|
|
240
279
|
local_ai_healthy() {
|
|
241
280
|
curl -fsS --max-time 3 "http://127.0.0.1:$SERVE_PORT/v1/models" 2>/dev/null \
|
|
242
281
|
| grep -Fq "$MODEL_ID"
|
|
@@ -483,7 +522,7 @@ if [ -f "$TEMPLATE" ]; then
|
|
|
483
522
|
-e "s#__PORT__#$SERVE_PORT#g" \
|
|
484
523
|
-e "s#__LOG_DIR__#$LOG_DIR#g" \
|
|
485
524
|
"$TEMPLATE" > "$TARGET" 2>/dev/null || log "WARN: could not render serve plist"
|
|
486
|
-
write_litertlm_config "gpu" || log "WARN: could not write $CONFIG_JSON - the serve will use its defaults"
|
|
525
|
+
write_litertlm_config "gpu" "true" || log "WARN: could not write $CONFIG_JSON - the serve will use its defaults"
|
|
487
526
|
restart_launch_agent \
|
|
488
527
|
|| { [ "$REQUIRE_BUNDLED_MODEL" = "1" ] && fail "local AI service could not be started"; log "WARN: launchctl bootstrap failed for serve agent"; }
|
|
489
528
|
else
|
|
@@ -491,26 +530,66 @@ else
|
|
|
491
530
|
log "WARN: serve plist template missing at $TEMPLATE"
|
|
492
531
|
fi
|
|
493
532
|
|
|
494
|
-
# The
|
|
495
|
-
# that route never builds an engine - so it cannot see a GPU vision encoder
|
|
496
|
-
# fails on first USE. One real image inference is the only thing that can,
|
|
497
|
-
# it does not answer we
|
|
498
|
-
# unable to sense at all
|
|
533
|
+
# The engine-settings cascade. `local_ai_healthy` only proves /v1/models answers,
|
|
534
|
+
# and that route never builds an engine - so it cannot see a GPU vision encoder
|
|
535
|
+
# that fails on first USE. One real image inference is the only thing that can,
|
|
536
|
+
# and if it does not answer we drop one optimism at a time rather than leave the
|
|
537
|
+
# host unable to sense at all.#
|
|
538
|
+
# THREE RUNGS, NOT TWO, AND THE THIRD IS THE SAME RULE AS THE FIRST. There are
|
|
539
|
+
# two optimistic engine settings in this file and only one of them was ever
|
|
540
|
+
# checked. Each rung drops exactly one and re-asks the same question, so a box
|
|
541
|
+
# ends on the configuration with the fewest unverified assumptions it can
|
|
542
|
+
# actually serve on:
|
|
543
|
+
#
|
|
544
|
+
# 1. vision_backend=gpu + speculative_decoding=true the measured fast path
|
|
545
|
+
# 2. speculative_decoding=true GPU vision unusable here
|
|
546
|
+
# 3. speculative_decoding=false neither is usable here
|
|
547
|
+
#
|
|
548
|
+
# Rung 3 can only turn a NON-SERVING box into a serving one: it is reached only
|
|
549
|
+
# after the box has already failed rung 2, which today is the give-up path.
|
|
550
|
+
# `speculative_decoding` is a draft-plus-verify scheme — strictly more compute
|
|
551
|
+
# per token, needing spare parallelism to win — and every number we have for it
|
|
552
|
+
# was measured on ONE machine (M4 Max: Metal for the 1.433x headline, its own CPU
|
|
553
|
+
# arm for the 2026-08-19 A/B). Dropping it costs speed on a box that has already
|
|
554
|
+
# failed twice, and a slow box is a box that is serving.
|
|
555
|
+
_wait_for_local_ai() {
|
|
556
|
+
for _attempt in $(seq 1 60); do
|
|
557
|
+
local_ai_healthy && return 0
|
|
558
|
+
sleep 2
|
|
559
|
+
done
|
|
560
|
+
return 1
|
|
561
|
+
}
|
|
562
|
+
|
|
499
563
|
litertlm_vision_cascade() {
|
|
500
564
|
if verify_litertlm_vision; then
|
|
501
565
|
log "local AI healthy with the GPU vision encoder"
|
|
502
566
|
return 0
|
|
503
567
|
fi
|
|
504
568
|
log "WARN: GPU vision encoder did not answer a probe inference - falling back to the CPU encoder"
|
|
505
|
-
write_litertlm_config "" || log "WARN: could not rewrite $CONFIG_JSON"
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
569
|
+
write_litertlm_config "" "true" || log "WARN: could not rewrite $CONFIG_JSON"
|
|
570
|
+
# A RUNG MAY ONLY BE GRADED IF ITS CONFIG IS THE ONE RUNNING. A restart that
|
|
571
|
+
# did not happen leaves the PREVIOUS rung's engine answering, so the probe
|
|
572
|
+
# below would fail for the previous rung's reason and descending on it would
|
|
573
|
+
# disable speculative decoding without ever having tested it.
|
|
574
|
+
if ! restart_launch_agent; then
|
|
575
|
+
log "WARN: could not restart the serve agent, so the config just written is NOT the one running"
|
|
576
|
+
log "WARN: leaving the CPU vision encoder on disk unevaluated - ensure-litertlm.sh re-runs this and grades it"
|
|
577
|
+
return 1
|
|
578
|
+
fi
|
|
579
|
+
if _wait_for_local_ai && verify_litertlm_plain_inference; then
|
|
580
|
+
log "local AI healthy on the CPU vision encoder (slower, but serving)"
|
|
581
|
+
return 0
|
|
582
|
+
fi
|
|
583
|
+
log "WARN: the CPU vision encoder did not answer either - retrying without speculative decoding"
|
|
584
|
+
write_litertlm_config "" "false" || log "WARN: could not rewrite $CONFIG_JSON"
|
|
585
|
+
if ! restart_launch_agent; then
|
|
586
|
+
log "WARN: could not restart the serve agent, so the config just written is NOT the one running"
|
|
587
|
+
return 1
|
|
588
|
+
fi
|
|
589
|
+
if _wait_for_local_ai && verify_litertlm_plain_inference; then
|
|
590
|
+
log "local AI healthy WITHOUT speculative decoding (slower still, but serving)"
|
|
591
|
+
return 0
|
|
592
|
+
fi
|
|
514
593
|
return 1
|
|
515
594
|
}
|
|
516
595
|
|
|
@@ -38,13 +38,41 @@ EOF
|
|
|
38
38
|
|
|
39
39
|
# Fake curl models BOTH endpoints the setup script uses:
|
|
40
40
|
# GET /v1/models -> the catalogue body `local_ai_healthy` greps
|
|
41
|
-
# POST /v1/chat/completions ->
|
|
42
|
-
#
|
|
41
|
+
# POST /v1/chat/completions -> a probe inference, which asks for `%{http_code}`
|
|
42
|
+
#
|
|
43
|
+
# IT TELLS THE TWO PROBES APART BY THE MODEL ID IN THE BODY, and it has to: the
|
|
44
|
+
# cascade's rungs differ in which id they post (`<id>,gpu` for the GPU-vision
|
|
45
|
+
# rung, the bare id afterwards) and a fake that answers both identically cannot
|
|
46
|
+
# exercise a cascade whose whole job is to distinguish them.
|
|
47
|
+
#
|
|
48
|
+
# TRILLBOARDS_TEST_VISION_PROBE_CODE the `,gpu` probe's status
|
|
49
|
+
# TRILLBOARDS_TEST_PLAIN_PROBE_CODE the bare-id probe's status
|
|
50
|
+
# TRILLBOARDS_TEST_FAIL_WHEN_SPECULATIVE=1
|
|
51
|
+
# a box that cannot infer at all while `speculative_decoding` is true. The
|
|
52
|
+
# fake reads the config the script just wrote, which is the only way to
|
|
53
|
+
# exercise a rung defined by what that file says.
|
|
43
54
|
cat >"$FAKE_BIN/curl" <<'EOF'
|
|
44
55
|
#!/bin/bash
|
|
56
|
+
body=""
|
|
57
|
+
prev=""
|
|
58
|
+
for arg in "$@"; do
|
|
59
|
+
case "$prev" in
|
|
60
|
+
--data-binary) body="${arg#@}" ;;
|
|
61
|
+
esac
|
|
62
|
+
prev="$arg"
|
|
63
|
+
done
|
|
45
64
|
for arg in "$@"; do
|
|
46
65
|
if [ "$arg" = "-w" ]; then
|
|
47
|
-
|
|
66
|
+
if [ "${TRILLBOARDS_TEST_FAIL_WHEN_SPECULATIVE:-0}" = "1" ] \
|
|
67
|
+
&& grep -Fq '"speculative_decoding":true' "$LITERT_LM_DIR/config.json" 2>/dev/null; then
|
|
68
|
+
printf '500'
|
|
69
|
+
exit 0
|
|
70
|
+
fi
|
|
71
|
+
if [ -n "$body" ] && grep -Fq ',gpu"' "$body" 2>/dev/null; then
|
|
72
|
+
printf '%s' "${TRILLBOARDS_TEST_VISION_PROBE_CODE:-200}"
|
|
73
|
+
else
|
|
74
|
+
printf '%s' "${TRILLBOARDS_TEST_PLAIN_PROBE_CODE:-200}"
|
|
75
|
+
fi
|
|
48
76
|
exit 0
|
|
49
77
|
fi
|
|
50
78
|
done
|
|
@@ -147,9 +175,10 @@ grep -Fq 'falling back to the CPU encoder' <<<"$OUTPUT2"
|
|
|
147
175
|
grep -Fq 'local AI healthy on the CPU vision encoder' <<<"$OUTPUT2"
|
|
148
176
|
grep -Fq 'local AI is installed and healthy' <<<"$OUTPUT2"
|
|
149
177
|
|
|
150
|
-
# The fallback must REMOVE vision_backend and KEEP speculative_decoding
|
|
151
|
-
#
|
|
152
|
-
#
|
|
178
|
+
# The vision fallback must REMOVE vision_backend and KEEP speculative_decoding.
|
|
179
|
+
# ONE RUNG DROPS ONE OPTIMISM: this box proved it can infer with speculative
|
|
180
|
+
# decoding on, so taking it away here would cost speed for a reason that was
|
|
181
|
+
# just disproved on this machine.
|
|
153
182
|
if grep -Fq 'vision_backend' "$LITERT_LM_DIR/config.json"; then
|
|
154
183
|
printf 'FAIL: fallback left vision_backend in config.json\n' >&2
|
|
155
184
|
cat "$LITERT_LM_DIR/config.json" >&2
|
|
@@ -163,5 +192,60 @@ grep -Fq '"speculative_decoding":true' "$LITERT_LM_DIR/config.json"
|
|
|
163
192
|
|
|
164
193
|
unset TRILLBOARDS_TEST_VISION_PROBE_CODE
|
|
165
194
|
|
|
166
|
-
|
|
195
|
+
# -- Scenario 2b: the CPU-VISION rung is verified by an INFERENCE, not by health
|
|
196
|
+
#
|
|
197
|
+
# `local_ai_healthy` only proves /v1/models answers, and this script's own
|
|
198
|
+
# comment says that route never builds an engine. Before this, the CPU-vision
|
|
199
|
+
# rung was accepted on health alone — so a box that fell back and STILL could not
|
|
200
|
+
# infer logged "healthy on the CPU vision encoder (slower, but serving)" and
|
|
201
|
+
# exited 0 while every frame 500s. That is how the UTF-8 BOM shipped as a green
|
|
202
|
+
# install on Windows, which is the platform that already re-probes here.
|
|
203
|
+
printf 'loaded\n' >"$TRILLBOARDS_TEST_STATE_DIR/service-state"
|
|
204
|
+
printf '0\n' >"$TRILLBOARDS_TEST_STATE_DIR/bootstrap-attempts"
|
|
205
|
+
export TRILLBOARDS_TEST_VISION_PROBE_CODE=500
|
|
206
|
+
export TRILLBOARDS_TEST_PLAIN_PROBE_CODE=500
|
|
207
|
+
if OUTPUT2B="$(/bin/bash "$SETUP" 2>&1)"; then
|
|
208
|
+
printf '%s\n' "$OUTPUT2B" >&2
|
|
209
|
+
printf 'FAIL: setup reported success on a box that could not infer on ANY rung\n' >&2
|
|
210
|
+
exit 1
|
|
211
|
+
fi
|
|
212
|
+
if grep -Fq 'local AI healthy on the CPU vision encoder' <<<"$OUTPUT2B"; then
|
|
213
|
+
printf 'FAIL: the CPU-vision rung was accepted on health alone\n' >&2
|
|
214
|
+
exit 1
|
|
215
|
+
fi
|
|
216
|
+
unset TRILLBOARDS_TEST_VISION_PROBE_CODE TRILLBOARDS_TEST_PLAIN_PROBE_CODE
|
|
217
|
+
|
|
218
|
+
# -- Scenario 3: a host that cannot infer while speculative decoding is on -----
|
|
219
|
+
#
|
|
220
|
+
# `speculative_decoding` is the second optimistic engine setting this file
|
|
221
|
+
# writes, and until 2026-09-04 it was written unconditionally `true` under a
|
|
222
|
+
# comment calling it "delegate independent" — a property measured on ONE machine
|
|
223
|
+
# (an M4 Max, both arms). This rung is the same rule the vision backend already
|
|
224
|
+
# gets: prove it on THIS box, and back it out if the serve cannot answer with it.
|
|
225
|
+
#
|
|
226
|
+
# It can only turn a NON-SERVING box into a serving one — it is reached only
|
|
227
|
+
# after the box has already failed the rung above, which used to be the give-up
|
|
228
|
+
# path.
|
|
229
|
+
printf 'loaded\n' >"$TRILLBOARDS_TEST_STATE_DIR/service-state"
|
|
230
|
+
printf '0\n' >"$TRILLBOARDS_TEST_STATE_DIR/bootstrap-attempts"
|
|
231
|
+
export TRILLBOARDS_TEST_FAIL_WHEN_SPECULATIVE=1
|
|
232
|
+
|
|
233
|
+
if ! OUTPUT3="$(/bin/bash "$SETUP" 2>&1)"; then
|
|
234
|
+
printf '%s\n' "$OUTPUT3" >&2
|
|
235
|
+
printf 'FAIL: setup exited non-zero on a box that serves without speculative decoding\n' >&2
|
|
236
|
+
exit 1
|
|
237
|
+
fi
|
|
238
|
+
|
|
239
|
+
grep -Fq 'retrying without speculative decoding' <<<"$OUTPUT3"
|
|
240
|
+
grep -Fq 'local AI healthy WITHOUT speculative decoding' <<<"$OUTPUT3"
|
|
241
|
+
grep -Fq 'local AI is installed and healthy' <<<"$OUTPUT3"
|
|
242
|
+
grep -Fq '"speculative_decoding":false' "$LITERT_LM_DIR/config.json"
|
|
243
|
+
if grep -Fq 'vision_backend' "$LITERT_LM_DIR/config.json"; then
|
|
244
|
+
printf 'FAIL: the speculative-decoding rung left vision_backend in config.json\n' >&2
|
|
245
|
+
exit 1
|
|
246
|
+
fi
|
|
247
|
+
|
|
248
|
+
unset TRILLBOARDS_TEST_FAIL_WHEN_SPECULATIVE
|
|
249
|
+
|
|
250
|
+
printf 'macOS LiteRT-LM reinstall race + engine-settings cascade regressions passed\n'
|
|
167
251
|
|
|
@@ -133,20 +133,39 @@ CONFIG_JSON="$LITERT_DIR/config.json"
|
|
|
133
133
|
LITERTLM_PROBE_JPEG_B64="/9j/4AAQSkZJRgABAgAAAQABAAD//gAPTGF2YzYzLjEuMTAxAP/bAEMACD4+ST5JVVVVVVVVZF1kaGhoZGRkZGhoaHBwcIODg3BwcGhocHB8fIODj5OPh4eDh5OTm5uburqystnZ4P/////EAEoAAQAAAAAAAAAAAAAAAAAAAAABAQAAAAAAAAAAAAAAAAAAAAAQAQAAAAAAAAAAAAAAAAAAAAARAQAAAAAAAAAAAAAAAAAAAAD/wAARCAAgACADASIAAhEAAxEA/9oADAMBAAIRAxEAPwAAAAAAAAD/2Q=="
|
|
134
134
|
|
|
135
135
|
# Write config.json. $1 = "gpu" pins the vision encoder; empty leaves it on the
|
|
136
|
-
# CPU default.
|
|
137
|
-
#
|
|
136
|
+
# CPU default. $2 = "true"/"false" for speculative decoding.
|
|
137
|
+
#
|
|
138
|
+
# BOTH ARE ALWAYS WRITTEN EXPLICITLY, AND THE SECOND ONE IS NEW.
|
|
139
|
+
#
|
|
140
|
+
# `speculative_decoding` used to be written unconditionally `true` under a
|
|
141
|
+
# comment asserting it is "delegate independent, so it survives the fallback".
|
|
142
|
+
# The evidence for that is two arms on ONE machine — an M4 Max, GPU/Metal for the
|
|
143
|
+
# headline 1.433x, and its own CPU arm for the 2026-08-19 A/B in
|
|
144
|
+
# `cctv-local-inference-hardware-floor.md`. Neither is a measurement of any box
|
|
145
|
+
# we sell, and this file already refuses to leave an unverified `vision_backend`
|
|
146
|
+
# on disk for exactly that reason. The rung below applies the same rule to this
|
|
147
|
+
# key: prove it on THIS box, and back it out if the serve cannot answer with it.
|
|
148
|
+
#
|
|
149
|
+
# EXPLICIT `false` RATHER THAN OMITTING THE KEY, and that is measured too:
|
|
150
|
+
# litert-lm 0.16.0 logs `Using speculative_decoding from config for model
|
|
151
|
+
# '<id>': <bool>` on every request when the key is PRESENT and says NOTHING AT
|
|
152
|
+
# ALL when it is absent (checked on a c4-standard-8, 2026-09-04). A box whose
|
|
153
|
+
# state is unreadable from its own log is a box whose next incident is diagnosed
|
|
154
|
+
# by guessing — which is what happened when a GCE box SEGV-looped on 2026-09-02
|
|
155
|
+
# and the config had to be edited by hand to find out what it had been doing.
|
|
138
156
|
write_litertlm_config() {
|
|
139
157
|
_vision="$1"
|
|
158
|
+
_speculative="${2:-true}"
|
|
140
159
|
mkdir -p "$LITERT_DIR" 2>/dev/null || true
|
|
141
160
|
if [ -n "$_vision" ]; then
|
|
142
|
-
printf '{"models":{"%s":{"vision_backend":"%s","speculative_decoding"
|
|
143
|
-
"$MODEL_ID" "$_vision" > "$CONFIG_JSON.tmp" 2>/dev/null || return 1
|
|
161
|
+
printf '{"models":{"%s":{"vision_backend":"%s","speculative_decoding":%s}}}\n' \
|
|
162
|
+
"$MODEL_ID" "$_vision" "$_speculative" > "$CONFIG_JSON.tmp" 2>/dev/null || return 1
|
|
144
163
|
else
|
|
145
|
-
printf '{"models":{"%s":{"speculative_decoding"
|
|
146
|
-
"$MODEL_ID" > "$CONFIG_JSON.tmp" 2>/dev/null || return 1
|
|
164
|
+
printf '{"models":{"%s":{"speculative_decoding":%s}}}\n' \
|
|
165
|
+
"$MODEL_ID" "$_speculative" > "$CONFIG_JSON.tmp" 2>/dev/null || return 1
|
|
147
166
|
fi
|
|
148
167
|
mv -f "$CONFIG_JSON.tmp" "$CONFIG_JSON" 2>/dev/null || return 1
|
|
149
|
-
log "wrote $CONFIG_JSON (vision_backend=${_vision:-cpu-default}, speculative_decoding
|
|
168
|
+
log "wrote $CONFIG_JSON (vision_backend=${_vision:-cpu-default}, speculative_decoding=$_speculative)"
|
|
150
169
|
}
|
|
151
170
|
|
|
152
171
|
# -- The one knob config.json cannot express -----------------------------------
|
|
@@ -245,6 +264,26 @@ verify_litertlm_vision() {
|
|
|
245
264
|
[ "$_code" = "200" ]
|
|
246
265
|
}
|
|
247
266
|
|
|
267
|
+
# The same probe, but asking for NO delegate suffix rather than `,gpu`.
|
|
268
|
+
#
|
|
269
|
+
# WHY A SECOND FUNCTION AND NOT A FLAG: `verify_litertlm_vision` posts
|
|
270
|
+
# `$MODEL_ID,gpu`, which is what the connector sends, and litert-lm returns 200
|
|
271
|
+
# for a delegate token it cannot honour — so on the later rungs that suffix would
|
|
272
|
+
# prove nothing about the thing being tested and could mask it. Here the model id
|
|
273
|
+
# carries no suffix at all, so a 200 means the box built an engine and ran a real
|
|
274
|
+
# image through it under the config now on disk, which is exactly the claim each
|
|
275
|
+
# rung needs.
|
|
276
|
+
verify_litertlm_plain_inference() {
|
|
277
|
+
_body="$LITERT_DIR/.plain-probe.json"
|
|
278
|
+
printf '{"model":"%s","messages":[{"role":"user","content":[{"type":"text","text":"Reply with the single word: ok"},{"type":"image_url","image_url":{"url":"data:image/jpeg;base64,%s"}}]}],"max_completion_tokens":8,"temperature":0}' \
|
|
279
|
+
"$MODEL_ID" "$LITERTLM_PROBE_JPEG_B64" > "$_body" 2>/dev/null || return 1
|
|
280
|
+
_code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 180 \
|
|
281
|
+
-X POST "http://127.0.0.1:$SERVE_PORT/v1/chat/completions" \
|
|
282
|
+
-H 'Content-Type: application/json' --data-binary @"$_body" 2>/dev/null)
|
|
283
|
+
rm -f "$_body" 2>/dev/null || true
|
|
284
|
+
[ "$_code" = "200" ]
|
|
285
|
+
}
|
|
286
|
+
|
|
248
287
|
local_ai_healthy() {
|
|
249
288
|
curl -fsS --max-time 3 "http://127.0.0.1:$SERVE_PORT/v1/models" 2>/dev/null \
|
|
250
289
|
| grep -Fq "$MODEL_ID"
|
|
@@ -350,15 +389,25 @@ fi
|
|
|
350
389
|
|
|
351
390
|
# 4. Serve unit. Installed by install.sh (which has sudo); here we only start it
|
|
352
391
|
# when we can, so a re-run from the service user still recovers a stopped serve.
|
|
392
|
+
# RETURNS NON-ZERO WHEN IT DID NOT RESTART, and the caller has to care.
|
|
393
|
+
#
|
|
394
|
+
# On the ORDINARY Linux install path this function cannot restart anything:
|
|
395
|
+
# `install.sh` invokes this script as the unprivileged `trillboards` service user
|
|
396
|
+
# (nologin, no sudoers entry, so `sudo -n` answers "a password is required"), and
|
|
397
|
+
# install.sh's own comment says so. It used to swallow that and return 0, which
|
|
398
|
+
# is fine for "start it if you can" and WRONG for a cascade: every rung after the
|
|
399
|
+
# first would then write a config, fail to apply it, and grade the STILL-RUNNING
|
|
400
|
+
# engine — reaching a verdict about a configuration nothing ever loaded.
|
|
353
401
|
restart_litertlm_serve() {
|
|
354
|
-
command -v systemctl >/dev/null 2>&1 || return
|
|
402
|
+
command -v systemctl >/dev/null 2>&1 || return 1
|
|
355
403
|
if systemctl list-unit-files "$SERVE_UNIT.service" >/dev/null 2>&1; then
|
|
356
|
-
systemctl restart "$SERVE_UNIT" >>"$LOG" 2>&1
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
log "WARN: $SERVE_UNIT.service is not installed yet (install.sh installs it)"
|
|
404
|
+
systemctl restart "$SERVE_UNIT" >>"$LOG" 2>&1 && return 0
|
|
405
|
+
sudo -n systemctl restart "$SERVE_UNIT" >>"$LOG" 2>&1 && return 0
|
|
406
|
+
log "WARN: could not restart $SERVE_UNIT - start it with: sudo systemctl restart $SERVE_UNIT"
|
|
407
|
+
return 1
|
|
361
408
|
fi
|
|
409
|
+
log "WARN: $SERVE_UNIT.service is not installed yet (install.sh installs it)"
|
|
410
|
+
return 1
|
|
362
411
|
}
|
|
363
412
|
|
|
364
413
|
wait_for_litertlm_health() {
|
|
@@ -373,7 +422,7 @@ wait_for_litertlm_health() {
|
|
|
373
422
|
# Ask for GPU vision BEFORE the restart so the engine builds with it. The engine
|
|
374
423
|
# is constructed lazily on the first inference, so config.json must be on disk
|
|
375
424
|
# before that request, not merely before the next boot.
|
|
376
|
-
write_litertlm_config "gpu" || log "WARN: could not write $CONFIG_JSON - the serve will use its defaults"
|
|
425
|
+
write_litertlm_config "gpu" "true" || log "WARN: could not write $CONFIG_JSON - the serve will use its defaults"
|
|
377
426
|
|
|
378
427
|
restart_litertlm_serve
|
|
379
428
|
|
|
@@ -384,16 +433,52 @@ if [ "$HEALTHY" -eq 1 ]; then
|
|
|
384
433
|
# The cascade. `local_ai_healthy` only proves /v1/models answers, which it does
|
|
385
434
|
# without ever building an engine - so it CANNOT see a GPU vision encoder that
|
|
386
435
|
# fails on first use. One real image inference is the only thing that can.
|
|
436
|
+
#
|
|
437
|
+
# THREE RUNGS, NOT TWO, AND THE THIRD IS THE SAME RULE AS THE FIRST. There are
|
|
438
|
+
# two optimistic engine settings in this file and only one of them was ever
|
|
439
|
+
# checked. Each rung drops exactly one and re-asks the same question, so a box
|
|
440
|
+
# ends on the configuration with the fewest unverified assumptions it can
|
|
441
|
+
# actually serve on:
|
|
442
|
+
#
|
|
443
|
+
# 1. vision_backend=gpu + speculative_decoding=true the measured fast path
|
|
444
|
+
# 2. speculative_decoding=true GPU vision unusable here
|
|
445
|
+
# 3. speculative_decoding=false neither is usable here
|
|
446
|
+
#
|
|
447
|
+
# Rung 3 can only turn a NON-SERVING box into a serving one: it is reached only
|
|
448
|
+
# after the box has already failed rung 2, which today is the give-up path.
|
|
449
|
+
# `speculative_decoding` is a draft-plus-verify scheme — strictly more compute
|
|
450
|
+
# per token, needing spare parallelism to win — and every number we have for it
|
|
451
|
+
# was measured on ONE machine (M4 Max: Metal for the 1.433x headline, its own CPU
|
|
452
|
+
# arm for the 2026-08-19 A/B). Dropping it costs speed on a box that has already
|
|
453
|
+
# failed twice, and a slow box is a box that is serving.
|
|
387
454
|
if verify_litertlm_vision; then
|
|
388
455
|
log "local AI healthy with the GPU vision encoder - the connector will run ON-DEVICE"
|
|
389
456
|
else
|
|
390
457
|
log "WARN: GPU vision encoder did not answer a probe inference - falling back to the CPU encoder"
|
|
391
|
-
write_litertlm_config "" || log "WARN: could not rewrite $CONFIG_JSON"
|
|
392
|
-
|
|
393
|
-
|
|
458
|
+
write_litertlm_config "" "true" || log "WARN: could not rewrite $CONFIG_JSON"
|
|
459
|
+
# A RUNG MAY ONLY BE GRADED IF ITS CONFIG IS THE ONE RUNNING. On the ordinary
|
|
460
|
+
# Linux install path this script runs unprivileged and cannot restart the
|
|
461
|
+
# unit, so the probe below would interrogate the engine built for the
|
|
462
|
+
# PREVIOUS rung and fail for the previous rung's reason - descending on that
|
|
463
|
+
# would disable speculative decoding without ever having tested it. So when
|
|
464
|
+
# the restart did not happen we STOP here, leaving the CPU-vision config on
|
|
465
|
+
# disk exactly as before: `install.sh` applies it with its own privileged
|
|
466
|
+
# restart and runs this script again, and that pass can restart and grade.
|
|
467
|
+
if ! restart_litertlm_serve; then
|
|
468
|
+
log "WARN: the serve could not be restarted here, so the config just written is NOT the one running"
|
|
469
|
+
log "WARN: leaving the CPU vision encoder on disk unevaluated - install.sh applies it and the next pass grades it"
|
|
470
|
+
elif wait_for_litertlm_health && verify_litertlm_plain_inference; then
|
|
394
471
|
log "local AI healthy on the CPU vision encoder (slower, but serving)"
|
|
395
472
|
else
|
|
396
|
-
|
|
473
|
+
log "WARN: the CPU vision encoder did not answer either - retrying without speculative decoding"
|
|
474
|
+
write_litertlm_config "" "false" || log "WARN: could not rewrite $CONFIG_JSON"
|
|
475
|
+
if ! restart_litertlm_serve; then
|
|
476
|
+
log "WARN: the serve could not be restarted here, so the config just written is NOT the one running"
|
|
477
|
+
elif wait_for_litertlm_health && verify_litertlm_plain_inference; then
|
|
478
|
+
log "local AI healthy WITHOUT speculative decoding (slower still, but serving)"
|
|
479
|
+
else
|
|
480
|
+
HEALTHY=0
|
|
481
|
+
fi
|
|
397
482
|
fi
|
|
398
483
|
fi
|
|
399
484
|
fi
|
|
@@ -412,9 +497,14 @@ if [ "$HEALTHY" -ne 1 ]; then
|
|
|
412
497
|
# works, which is the right trade for a value we could not check: a box that is
|
|
413
498
|
# slow is a box that is serving, and `setup-litertlm.sh` re-runs on every
|
|
414
499
|
# install.sh, which will promote it to GPU the moment it can prove GPU works.
|
|
415
|
-
write_litertlm_config "" \
|
|
500
|
+
write_litertlm_config "" "false" \
|
|
416
501
|
|| log "WARN: could not rewrite $CONFIG_JSON to the CPU default"
|
|
417
|
-
|
|
502
|
+
# BOTH optimisms are reset, for one reason: neither was checked. The serve
|
|
503
|
+
# never came up, so the cascade that would have proved either of them never
|
|
504
|
+
# ran, and the file OUTLIVES this install — the unit is enabled, so the next
|
|
505
|
+
# boot starts on a configuration nobody confirmed. `setup-litertlm.sh` re-runs
|
|
506
|
+
# on every install.sh and promotes both again the moment it can prove them.
|
|
507
|
+
log "vision_backend and speculative_decoding left UNVERIFIED, so both were reset to the conservative default"
|
|
418
508
|
|
|
419
509
|
# NOT fatal. auto-sense waits and beacons, so an install that gets this far
|
|
420
510
|
# with the model on disk recovers by itself once the serve comes up.
|
|
@@ -166,17 +166,33 @@ $script:LitertlmProbeJpegB64 = "/9j/4AAQSkZJRgABAgAAAQABAAD//gAPTGF2YzYzLjEuMTAx
|
|
|
166
166
|
function Get-LitertlmConfigPath { Join-Path $LitertDir "config.json" }
|
|
167
167
|
|
|
168
168
|
# $VisionBackend = "gpu" pins the vision encoder; "" leaves it on the CPU
|
|
169
|
-
# default.
|
|
170
|
-
#
|
|
169
|
+
# default. $SpeculativeDecoding is the second engine setting this file writes.
|
|
170
|
+
#
|
|
171
|
+
# BOTH ARE ALWAYS WRITTEN EXPLICITLY, AND THE SECOND ONE IS NEW.
|
|
172
|
+
#
|
|
173
|
+
# speculative_decoding used to be written unconditionally $true under a comment
|
|
174
|
+
# asserting it is "delegate independent, so it survives the fallback". The
|
|
175
|
+
# evidence for that is two arms on ONE machine - an M4 Max, Metal for the
|
|
176
|
+
# headline 1.433x and its own CPU arm for the 2026-08-19 A/B in
|
|
177
|
+
# cctv-local-inference-hardware-floor.md. Neither is a measurement of any box we
|
|
178
|
+
# sell, and this file already refuses to leave an unverified vision_backend on
|
|
179
|
+
# disk for exactly that reason. The cascade below applies the same rule here.
|
|
180
|
+
#
|
|
181
|
+
# EXPLICIT $false RATHER THAN OMITTING THE KEY, and that is measured too:
|
|
182
|
+
# litert-lm 0.16.0 logs "Using speculative_decoding from config for model
|
|
183
|
+
# '<id>': <bool>" on every request when the key is PRESENT and says NOTHING AT
|
|
184
|
+
# ALL when it is absent (checked on a c4-standard-8, 2026-09-04). A box whose
|
|
185
|
+
# state is unreadable from its own log is a box whose next incident is diagnosed
|
|
186
|
+
# by guessing.
|
|
171
187
|
function Write-LitertlmConfig {
|
|
172
|
-
param([string]$VisionBackend = "")
|
|
188
|
+
param([string]$VisionBackend = "", [bool]$SpeculativeDecoding = $true)
|
|
173
189
|
try {
|
|
174
190
|
if (-not (Test-Path -LiteralPath $LitertDir)) {
|
|
175
191
|
New-Item -ItemType Directory -Force -Path $LitertDir | Out-Null
|
|
176
192
|
}
|
|
177
193
|
$model = [ordered]@{}
|
|
178
194
|
if ($VisionBackend) { $model["vision_backend"] = $VisionBackend }
|
|
179
|
-
$model["speculative_decoding"] = $
|
|
195
|
+
$model["speculative_decoding"] = $SpeculativeDecoding
|
|
180
196
|
$cfg = [ordered]@{ models = [ordered]@{ $ModelId = $model } }
|
|
181
197
|
$path = Get-LitertlmConfigPath
|
|
182
198
|
$tmp = "$path.tmp"
|
|
@@ -198,7 +214,7 @@ function Write-LitertlmConfig {
|
|
|
198
214
|
(New-Object System.Text.UTF8Encoding($false)))
|
|
199
215
|
Move-Item -LiteralPath $tmp -Destination $path -Force
|
|
200
216
|
$label = if ($VisionBackend) { $VisionBackend } else { "cpu-default" }
|
|
201
|
-
Log "wrote $path (vision_backend=$label, speculative_decoding
|
|
217
|
+
Log "wrote $path (vision_backend=$label, speculative_decoding=$SpeculativeDecoding)"
|
|
202
218
|
return $true
|
|
203
219
|
} catch {
|
|
204
220
|
Log "WARN: could not write litert-lm config.json: $_"
|
|
@@ -670,7 +686,7 @@ if (-not (Test-Path -LiteralPath $registration)) {
|
|
|
670
686
|
|
|
671
687
|
# Ask for GPU vision BEFORE the serve starts: the engine is built lazily on the
|
|
672
688
|
# first inference, so config.json must be on disk before that request.
|
|
673
|
-
Write-LitertlmConfig -VisionBackend "gpu" | Out-Null
|
|
689
|
+
Write-LitertlmConfig -VisionBackend "gpu" -SpeculativeDecoding $true | Out-Null
|
|
674
690
|
|
|
675
691
|
Log "registering litert-lm serve Scheduled Task on :$Port"
|
|
676
692
|
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $registration -LitertLmPath $litertlmPath -Port $Port *>> $LogPath
|
|
@@ -698,7 +714,7 @@ if (-not $healthy) {
|
|
|
698
714
|
# encoder rather than leave this host unable to sense at all.
|
|
699
715
|
if (-not (Test-LitertlmVision -InferenceModel "$ModelId,gpu")) {
|
|
700
716
|
Log "WARN: GPU vision encoder did not answer a probe inference; falling back to the CPU encoder"
|
|
701
|
-
Write-LitertlmConfig | Out-Null
|
|
717
|
+
Write-LitertlmConfig -SpeculativeDecoding $true | Out-Null
|
|
702
718
|
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $registration -LitertLmPath $litertlmPath -Port $Port *>> $LogPath
|
|
703
719
|
$healthy = $false
|
|
704
720
|
for ($attempt = 0; $attempt -lt 60 -and -not $healthy; $attempt++) {
|
|
@@ -730,9 +746,46 @@ if (-not (Test-LitertlmVision -InferenceModel "$ModelId,gpu")) {
|
|
|
730
746
|
} elseif (Test-LitertlmVision -InferenceModel $ModelId) {
|
|
731
747
|
Log "local AI healthy on the full CPU fallback (slower, but serving)"
|
|
732
748
|
} else {
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
749
|
+
# THE THIRD RUNG, AND IT IS THE SAME RULE AS THE FIRST. There are two
|
|
750
|
+
# optimistic engine settings in this file and only one of them was ever
|
|
751
|
+
# checked. Each rung drops exactly one and re-asks the same question, so a
|
|
752
|
+
# box ends on the configuration with the fewest unverified assumptions it can
|
|
753
|
+
# actually serve on:
|
|
754
|
+
#
|
|
755
|
+
# 1. vision_backend=gpu + speculative_decoding=true the measured fast path
|
|
756
|
+
# 2. speculative_decoding=true GPU vision unusable here
|
|
757
|
+
# 3. speculative_decoding=false neither is usable here
|
|
758
|
+
#
|
|
759
|
+
# This can only turn a box that was about to FAIL THE INSTALL into a serving
|
|
760
|
+
# one: it is reached only where the previous code called Fail-LocalAi.
|
|
761
|
+
# speculative_decoding is a draft-plus-verify scheme - strictly more compute
|
|
762
|
+
# per token, needing spare parallelism to win - and every number we have for
|
|
763
|
+
# it was measured on ONE machine. Dropping it costs speed on a box that has
|
|
764
|
+
# already failed twice, and a slow box is a box that is serving.
|
|
765
|
+
Log "WARN: neither GPU inference nor the full CPU fallback answered; retrying without speculative decoding"
|
|
766
|
+
Write-LitertlmConfig -SpeculativeDecoding $false | Out-Null
|
|
767
|
+
# A RUNG MAY ONLY BE GRADED IF ITS CONFIG IS THE ONE RUNNING. Re-registering
|
|
768
|
+
# the Scheduled Task is what applies the file; if that fails, the engine
|
|
769
|
+
# still answering is the previous rung's and grading it would reach a verdict
|
|
770
|
+
# about a configuration nothing ever loaded.
|
|
771
|
+
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $registration -LitertLmPath $litertlmPath -Port $Port *>> $LogPath
|
|
772
|
+
if ($LASTEXITCODE -ne 0) {
|
|
773
|
+
Fail-LocalAi "Gemma is installed, but the local AI service could not be restarted to apply the fallback configuration. Restart Windows, then retry Local AI. See $LogPath."
|
|
774
|
+
Stop-SetupLock
|
|
775
|
+
exit 1
|
|
776
|
+
}
|
|
777
|
+
$healthy = $false
|
|
778
|
+
for ($attempt = 0; $attempt -lt 60 -and -not $healthy; $attempt++) {
|
|
779
|
+
$healthy = Test-ServeHealthy
|
|
780
|
+
if (-not $healthy) { Start-Sleep -Seconds 2 }
|
|
781
|
+
}
|
|
782
|
+
if ($healthy -and (Test-LitertlmVision -InferenceModel $ModelId)) {
|
|
783
|
+
Log "local AI healthy WITHOUT speculative decoding (slower still, but serving)"
|
|
784
|
+
} else {
|
|
785
|
+
Fail-LocalAi "Gemma is installed and the local AI service is answering, but it could not analyze a test image with GPU inference, the full CPU fallback, or with speculative decoding disabled. See $LogPath."
|
|
786
|
+
Stop-SetupLock
|
|
787
|
+
exit 1
|
|
788
|
+
}
|
|
736
789
|
}
|
|
737
790
|
} else {
|
|
738
791
|
Log "local AI healthy with the GPU vision encoder"
|
package/dist/EdgeAgent.js
CHANGED
|
@@ -44,7 +44,7 @@ const edge_ads_1 = require("@trillboards/edge-ads");
|
|
|
44
44
|
const CommandRouter_1 = require("./CommandRouter");
|
|
45
45
|
const config_1 = require("./config");
|
|
46
46
|
const dataDir_1 = require("./dataDir");
|
|
47
|
-
const SDK_VERSION = '0.2.
|
|
47
|
+
const SDK_VERSION = '0.2.135';
|
|
48
48
|
class EdgeAgent extends events_1.EventEmitter {
|
|
49
49
|
constructor(config, deps) {
|
|
50
50
|
super();
|
package/dist/diagnose.d.ts
CHANGED
|
@@ -31,7 +31,7 @@ export interface DiagnosticReport {
|
|
|
31
31
|
skipped: number;
|
|
32
32
|
};
|
|
33
33
|
}
|
|
34
|
-
declare const SDK_VERSION = "0.2.
|
|
34
|
+
declare const SDK_VERSION = "0.2.135";
|
|
35
35
|
declare function checkNodeVersion(): CheckResult;
|
|
36
36
|
declare function checkPlatform(): CheckResult;
|
|
37
37
|
declare function checkMemory(): CheckResult;
|
package/dist/diagnose.js
CHANGED
|
@@ -63,7 +63,7 @@ const localNetworkAccess_1 = require("./localNetworkAccess");
|
|
|
63
63
|
// ---------------------------------------------------------------------------
|
|
64
64
|
// SDK version — mirrors package.json; kept in sync by CI
|
|
65
65
|
// ---------------------------------------------------------------------------
|
|
66
|
-
const SDK_VERSION = '0.2.
|
|
66
|
+
const SDK_VERSION = '0.2.135';
|
|
67
67
|
exports._SDK_VERSION = SDK_VERSION;
|
|
68
68
|
// ---------------------------------------------------------------------------
|
|
69
69
|
// ANSI helpers (no external deps)
|
package/package.json
CHANGED