@trillboards/edge-sdk 0.2.61 → 0.2.63
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/litertlm/test_trillboards_litertlm_shim.py +132 -0
- package/deploy/litertlm/trillboards-litertlm-shim.pth +1 -0
- package/deploy/litertlm/trillboards_litertlm_shim.py +214 -0
- package/deploy/macos/bench-local-vlm.sh +8 -4
- package/deploy/macos/build-macos-pkg.sh +4 -0
- package/deploy/macos/scripts/setup-litertlm.sh +99 -2
- package/deploy/systemd/setup-litertlm.sh +99 -2
- package/deploy/windows/build-windows.ps1 +4 -0
- package/deploy/windows/register-litertlm-task.ps1 +22 -0
- package/deploy/windows/setup-litertlm.ps1 +131 -3
- package/deploy/windows/trillboards-edge.cmd +19 -0
- package/dist/CloudSensingManager.d.ts +61 -0
- package/dist/CloudSensingManager.d.ts.map +1 -1
- package/dist/CloudSensingManager.js +52 -30
- package/dist/CloudSensingManager.js.map +1 -1
- package/dist/EdgeAgent.js +1 -1
- package/dist/LiteRtVlmProvider.d.ts +74 -7
- package/dist/LiteRtVlmProvider.d.ts.map +1 -1
- package/dist/LiteRtVlmProvider.js.map +1 -1
- package/dist/MosaicSource.d.ts +18 -0
- package/dist/MosaicSource.d.ts.map +1 -1
- package/dist/MosaicSource.js +23 -0
- package/dist/MosaicSource.js.map +1 -1
- package/dist/analyzeCostObserver.d.ts +96 -0
- package/dist/analyzeCostObserver.d.ts.map +1 -0
- package/dist/analyzeCostObserver.js +130 -0
- package/dist/analyzeCostObserver.js.map +1 -0
- package/dist/connectorCadence.d.ts +286 -57
- package/dist/connectorCadence.d.ts.map +1 -1
- package/dist/connectorCadence.js +313 -61
- package/dist/connectorCadence.js.map +1 -1
- package/dist/deployedCadence.d.ts +8 -2
- package/dist/deployedCadence.d.ts.map +1 -1
- package/dist/deployedCadence.js +6 -3
- package/dist/deployedCadence.js.map +1 -1
- package/dist/deployedCadenceSource.d.ts +10 -0
- package/dist/deployedCadenceSource.d.ts.map +1 -1
- package/dist/deployedCadenceSource.js +11 -0
- package/dist/deployedCadenceSource.js.map +1 -1
- package/dist/diagnose.d.ts +1 -1
- package/dist/diagnose.js +1 -1
- package/dist/localInferenceSchema.d.ts +8 -3
- package/dist/localInferenceSchema.d.ts.map +1 -1
- package/dist/localInferenceSchema.js +8 -3
- package/dist/localInferenceSchema.js.map +1 -1
- package/dist/localSensing.d.ts +22 -1
- package/dist/localSensing.d.ts.map +1 -1
- package/dist/localSensing.js +101 -7
- package/dist/localSensing.js.map +1 -1
- package/dist/localVlmBench.d.ts +82 -0
- package/dist/localVlmBench.d.ts.map +1 -1
- package/dist/localVlmBench.js +88 -0
- package/dist/localVlmBench.js.map +1 -1
- package/dist/sensingCatchRate.d.ts.map +1 -1
- package/dist/sensingCatchRate.js +75 -10
- package/dist/sensingCatchRate.js.map +1 -1
- package/dist/sensingRunMode.d.ts.map +1 -1
- package/dist/sensingRunMode.js +26 -2
- package/dist/sensingRunMode.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Fixture tests for the litert-lm constrained-decoding shim.
|
|
2
|
+
|
|
3
|
+
Runs with no litert-lm installed: a stand-in `litert_lm` module is planted on
|
|
4
|
+
sys.path so the import hook is exercised exactly as it is in production, and
|
|
5
|
+
the recorded kwargs prove WHICH configs are dropped and which survive.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import importlib
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
import tempfile
|
|
12
|
+
import textwrap
|
|
13
|
+
import unittest
|
|
14
|
+
|
|
15
|
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
16
|
+
sys.path.insert(0, HERE)
|
|
17
|
+
|
|
18
|
+
FAKE_LITERT_LM = textwrap.dedent(
|
|
19
|
+
'''
|
|
20
|
+
class ConstrainedDecodingConfig:
|
|
21
|
+
def __init__(self, enable=False, provider=None):
|
|
22
|
+
self.enable = enable
|
|
23
|
+
self.provider = provider
|
|
24
|
+
|
|
25
|
+
class Engine:
|
|
26
|
+
last_kwargs = None
|
|
27
|
+
|
|
28
|
+
def create_conversation(self, **kwargs):
|
|
29
|
+
Engine.last_kwargs = dict(kwargs)
|
|
30
|
+
return "conversation"
|
|
31
|
+
'''
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ShimTest(unittest.TestCase):
|
|
36
|
+
def setUp(self):
|
|
37
|
+
self.tmp = tempfile.TemporaryDirectory()
|
|
38
|
+
self.addCleanup(self.tmp.cleanup)
|
|
39
|
+
with open(os.path.join(self.tmp.name, "litert_lm.py"), "w") as fh:
|
|
40
|
+
fh.write(FAKE_LITERT_LM)
|
|
41
|
+
sys.path.insert(0, self.tmp.name)
|
|
42
|
+
self.addCleanup(lambda: sys.path.remove(self.tmp.name))
|
|
43
|
+
for name in ("litert_lm", "trillboards_litertlm_shim"):
|
|
44
|
+
sys.modules.pop(name, None)
|
|
45
|
+
self.addCleanup(lambda: [sys.modules.pop(n, None) for n in ("litert_lm", "trillboards_litertlm_shim")])
|
|
46
|
+
self._meta = list(sys.meta_path)
|
|
47
|
+
self.addCleanup(lambda: sys.meta_path.__setitem__(slice(None), self._meta))
|
|
48
|
+
|
|
49
|
+
def _load(self, **env):
|
|
50
|
+
old = {k: os.environ.get(k) for k in env}
|
|
51
|
+
os.environ.update({k: v for k, v in env.items() if v is not None})
|
|
52
|
+
for k, v in env.items():
|
|
53
|
+
if v is None:
|
|
54
|
+
os.environ.pop(k, None)
|
|
55
|
+
|
|
56
|
+
def restore():
|
|
57
|
+
for k, v in old.items():
|
|
58
|
+
if v is None:
|
|
59
|
+
os.environ.pop(k, None)
|
|
60
|
+
else:
|
|
61
|
+
os.environ[k] = v
|
|
62
|
+
|
|
63
|
+
self.addCleanup(restore)
|
|
64
|
+
return importlib.import_module("trillboards_litertlm_shim")
|
|
65
|
+
|
|
66
|
+
# ── the rule itself ────────────────────────────────────────────────────
|
|
67
|
+
def test_drops_only_the_no_response_format_shape(self):
|
|
68
|
+
shim = self._load(TRILLBOARDS_LITERTLM_SHIM="1")
|
|
69
|
+
import litert_lm
|
|
70
|
+
|
|
71
|
+
Cfg = litert_lm.ConstrainedDecodingConfig
|
|
72
|
+
self.assertTrue(shim.should_drop(Cfg(enable=True, provider=None)))
|
|
73
|
+
# a caller who asked for a response_format keeps their grammar
|
|
74
|
+
self.assertFalse(shim.should_drop(Cfg(enable=True, provider="LL_GUIDANCE")))
|
|
75
|
+
self.assertFalse(shim.should_drop(Cfg(enable=False, provider=None)))
|
|
76
|
+
self.assertFalse(shim.should_drop(None))
|
|
77
|
+
# truthy-but-not-True must NOT be treated as the upstream shape
|
|
78
|
+
self.assertFalse(shim.should_drop(Cfg(enable=1, provider=None)))
|
|
79
|
+
|
|
80
|
+
# ── the wrap, through a real import ────────────────────────────────────
|
|
81
|
+
def test_import_hook_wraps_and_drops(self):
|
|
82
|
+
self._load(TRILLBOARDS_LITERTLM_SHIM="1")
|
|
83
|
+
import litert_lm
|
|
84
|
+
|
|
85
|
+
cfg = litert_lm.ConstrainedDecodingConfig(enable=True, provider=None)
|
|
86
|
+
litert_lm.Engine().create_conversation(messages=[], constrained_decoding_config=cfg)
|
|
87
|
+
self.assertIsNone(litert_lm.Engine.last_kwargs["constrained_decoding_config"])
|
|
88
|
+
# every other kwarg is passed through untouched
|
|
89
|
+
self.assertEqual(litert_lm.Engine.last_kwargs["messages"], [])
|
|
90
|
+
|
|
91
|
+
def test_import_hook_preserves_a_real_response_format_request(self):
|
|
92
|
+
self._load(TRILLBOARDS_LITERTLM_SHIM="1")
|
|
93
|
+
import litert_lm
|
|
94
|
+
|
|
95
|
+
cfg = litert_lm.ConstrainedDecodingConfig(enable=True, provider="LL_GUIDANCE")
|
|
96
|
+
litert_lm.Engine().create_conversation(constrained_decoding_config=cfg)
|
|
97
|
+
self.assertIs(litert_lm.Engine.last_kwargs["constrained_decoding_config"], cfg)
|
|
98
|
+
|
|
99
|
+
def test_kill_switch_leaves_stock_behaviour(self):
|
|
100
|
+
self._load(TRILLBOARDS_LITERTLM_SHIM="0")
|
|
101
|
+
import litert_lm
|
|
102
|
+
|
|
103
|
+
cfg = litert_lm.ConstrainedDecodingConfig(enable=True, provider=None)
|
|
104
|
+
litert_lm.Engine().create_conversation(constrained_decoding_config=cfg)
|
|
105
|
+
self.assertIs(litert_lm.Engine.last_kwargs["constrained_decoding_config"], cfg)
|
|
106
|
+
|
|
107
|
+
def test_apply_is_idempotent(self):
|
|
108
|
+
shim = self._load(TRILLBOARDS_LITERTLM_SHIM="1")
|
|
109
|
+
import litert_lm
|
|
110
|
+
|
|
111
|
+
first = litert_lm.Engine.create_conversation
|
|
112
|
+
self.assertTrue(shim.apply_to(litert_lm))
|
|
113
|
+
self.assertIs(litert_lm.Engine.create_conversation, first)
|
|
114
|
+
|
|
115
|
+
# ── fails OPEN, never closed ───────────────────────────────────────────
|
|
116
|
+
def test_missing_engine_attribute_does_not_raise(self):
|
|
117
|
+
shim = self._load(TRILLBOARDS_LITERTLM_SHIM="1")
|
|
118
|
+
|
|
119
|
+
class NoEngine:
|
|
120
|
+
pass
|
|
121
|
+
|
|
122
|
+
self.assertFalse(shim.apply_to(NoEngine()))
|
|
123
|
+
|
|
124
|
+
def test_install_never_raises(self):
|
|
125
|
+
shim = self._load(TRILLBOARDS_LITERTLM_SHIM="1")
|
|
126
|
+
# a hostile meta_path must not take the serve down
|
|
127
|
+
sys.meta_path.insert(0, object())
|
|
128
|
+
self.assertIn(shim.install(), (True, False))
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
if __name__ == "__main__":
|
|
132
|
+
unittest.main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import trillboards_litertlm_shim
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""Drop the constrained-decoding setup `litert-lm serve` pays on EVERY request.
|
|
2
|
+
|
|
3
|
+
WHY THIS FILE EXISTS
|
|
4
|
+
--------------------
|
|
5
|
+
`litert-lm serve`'s OpenAI shim builds, once per HTTP request, at both
|
|
6
|
+
`openai_handler.py` construction sites (`/v1/chat/completions` and
|
|
7
|
+
`/v1/responses`, 0.15.0 / 0.16.0 / 0.16.1 alike)::
|
|
8
|
+
|
|
9
|
+
provider = LL_GUIDANCE if response_format is not None else None
|
|
10
|
+
constrained_decoding_config = ConstrainedDecodingConfig(enable=True, provider=provider)
|
|
11
|
+
with engine.create_conversation(..., constrained_decoding_config=cfg) as conv:
|
|
12
|
+
|
|
13
|
+
`enable` is **unconditionally True** — including when the caller sent no
|
|
14
|
+
`response_format` at all and the generation that follows is therefore
|
|
15
|
+
unconstrained. Enabling it makes the native `litert_lm_conversation_create`
|
|
16
|
+
build the constraint machinery for a grammar nothing will use, and it is
|
|
17
|
+
rebuilt for every conversation because the serve creates one per request.
|
|
18
|
+
|
|
19
|
+
MEASURED (Apple M4 Max 16c/48G, litert-lm 0.16.0, Gemma 4 E2B, GPU/Metal
|
|
20
|
+
delegate, vision_backend=gpu + speculative_decoding=true from config.json, the
|
|
21
|
+
two committed CCTV fixtures, operator/SOP field set, one engine, arms alternated
|
|
22
|
+
sample-by-sample with the leading arm rotated each round, n=10 pairs, load
|
|
23
|
+
average 22):
|
|
24
|
+
|
|
25
|
+
arm create_conversation send_message total
|
|
26
|
+
stock (enable=True) 0.717 s 0.560 s 1.275 s
|
|
27
|
+
this shim (dropped) 0.001 s 0.603 s 0.604 s
|
|
28
|
+
|
|
29
|
+
paired total ratio 2.194x (min 2.040, max 2.284)
|
|
30
|
+
generations BYTE-IDENTICAL 10/10; prefill 493 and decode 58 tokens on both
|
|
31
|
+
|
|
32
|
+
`send_message` is untouched — this removes setup, not sampling — and the
|
|
33
|
+
answers are the same bytes, which is the property that makes it safe.
|
|
34
|
+
|
|
35
|
+
THE RATIO IS ITSELF LOAD-DEPENDENT, and that is worth knowing before quoting
|
|
36
|
+
one: the same paired comparison through the shipped HTTP path gives 3.734x on a
|
|
37
|
+
box at 0.0% CPU idle and 2.42x at load 25, because the work this removes is
|
|
38
|
+
CPU-bound while the work it leaves is GPU-bound. The near-idle figure is the
|
|
39
|
+
honest one; a number taken under contention flatters this change.
|
|
40
|
+
|
|
41
|
+
The connector never sends `response_format`: LiteRtVlmProvider gets its
|
|
42
|
+
structured output by asking for a bare JSON object and validating client-side,
|
|
43
|
+
and it re-measured `response_format` at +35.2% p50 and DECLINED it. So on the
|
|
44
|
+
shipped path this setup is pure waste, every frame.
|
|
45
|
+
|
|
46
|
+
WHAT IT DOES NOT TOUCH
|
|
47
|
+
----------------------
|
|
48
|
+
The rule fires on EXACTLY `enable is True and provider is None` — the shape the
|
|
49
|
+
shim builds when, and only when, the caller asked for no `response_format`. A
|
|
50
|
+
request that DOES carry one arrives as `provider=LL_GUIDANCE` and is passed
|
|
51
|
+
through unchanged, so constrained decoding still works for anyone who asks for
|
|
52
|
+
it. `ConstrainedDecodingConfig` is constructed nowhere else in the litert-lm
|
|
53
|
+
CLI (`run.py` and `gemini_handler.py` create conversations without one), so no
|
|
54
|
+
other command can reach this wrap.
|
|
55
|
+
|
|
56
|
+
HOW IT IS LOADED
|
|
57
|
+
----------------
|
|
58
|
+
The three provisioners copy this file plus a one-line `.pth` into the litert-lm
|
|
59
|
+
tool venv's site-packages. That leaves every upstream file byte-identical on
|
|
60
|
+
disk, needs no change to how the serve is launched on any platform (the macOS
|
|
61
|
+
LaunchAgent, the systemd unit and the Windows Scheduled Task all still exec
|
|
62
|
+
`litert-lm serve`), and covers a serve started by hand as well as one started
|
|
63
|
+
by a service manager.
|
|
64
|
+
|
|
65
|
+
It is LAZY on purpose: `install()` only registers a `sys.meta_path` hook and
|
|
66
|
+
imports nothing, so it piggybacks on the import the CLI was going to do anyway
|
|
67
|
+
rather than adding one. `litert-lm list` and `litert-lm --version` — which the
|
|
68
|
+
provisioners run as health checks — keep exactly the startup cost they had.
|
|
69
|
+
|
|
70
|
+
It also fails OPEN. Every failure path leaves stock behaviour intact and prints
|
|
71
|
+
one line to stderr, which the serve's own log captures: a serve that is merely
|
|
72
|
+
slow keeps sensing, and a serve that refuses to start senses nothing. The line
|
|
73
|
+
is the only way to tell the two apart from the outside, so it is not optional —
|
|
74
|
+
grep the serve log for `[trillboards-litertlm-shim]`.
|
|
75
|
+
|
|
76
|
+
Set `TRILLBOARDS_LITERTLM_SHIM=0` to disable it without uninstalling anything.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
from __future__ import annotations
|
|
80
|
+
|
|
81
|
+
import os
|
|
82
|
+
import sys
|
|
83
|
+
|
|
84
|
+
__all__ = ["install", "apply_to", "should_drop", "TAG"]
|
|
85
|
+
|
|
86
|
+
TAG = "[trillboards-litertlm-shim]"
|
|
87
|
+
|
|
88
|
+
#: The module whose import triggers the wrap. `Engine.create_conversation` is
|
|
89
|
+
#: the single funnel every serve request goes through.
|
|
90
|
+
TARGET_MODULE = "litert_lm"
|
|
91
|
+
|
|
92
|
+
#: Marker set on the wrapper so a second install() is a no-op rather than a
|
|
93
|
+
#: second layer of indirection.
|
|
94
|
+
_MARKER = "_trillboards_litertlm_shim"
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _say(message: str) -> None:
|
|
98
|
+
"""One line to stderr. Never raises — a logging failure must not be fatal."""
|
|
99
|
+
try:
|
|
100
|
+
sys.stderr.write(f"{TAG} {message}\n")
|
|
101
|
+
sys.stderr.flush()
|
|
102
|
+
except Exception: # pragma: no cover - stderr is gone; nothing to do
|
|
103
|
+
pass
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _enabled() -> bool:
|
|
107
|
+
"""False only when explicitly switched off, so a missing var means ON."""
|
|
108
|
+
return os.environ.get("TRILLBOARDS_LITERTLM_SHIM", "1").strip() not in ("0", "false", "no")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def should_drop(config: object) -> bool:
|
|
112
|
+
"""Is this the no-response_format shape litert-lm builds on every request?
|
|
113
|
+
|
|
114
|
+
Deliberately exact. `enable is True` and not merely truthy, `provider is
|
|
115
|
+
None` and not merely falsy: anything else is a caller who asked for
|
|
116
|
+
constrained decoding, and dropping theirs would change an answer rather
|
|
117
|
+
than a setup cost.
|
|
118
|
+
"""
|
|
119
|
+
if config is None:
|
|
120
|
+
return False
|
|
121
|
+
return getattr(config, "enable", None) is True and getattr(config, "provider", "?") is None
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def apply_to(litert_lm_module: object) -> bool:
|
|
125
|
+
"""Wrap `Engine.create_conversation` on an imported litert_lm. Idempotent.
|
|
126
|
+
|
|
127
|
+
Returns True when the wrap is in place (including when it already was).
|
|
128
|
+
"""
|
|
129
|
+
engine_cls = getattr(litert_lm_module, "Engine", None)
|
|
130
|
+
original = getattr(engine_cls, "create_conversation", None)
|
|
131
|
+
if original is None:
|
|
132
|
+
_say("NOT ACTIVE: litert_lm.Engine.create_conversation is absent — "
|
|
133
|
+
"upstream shape changed; serving with stock (slower) behaviour")
|
|
134
|
+
return False
|
|
135
|
+
if getattr(original, _MARKER, False):
|
|
136
|
+
return True
|
|
137
|
+
|
|
138
|
+
def create_conversation(self, *args, **kwargs):
|
|
139
|
+
if should_drop(kwargs.get("constrained_decoding_config")):
|
|
140
|
+
kwargs["constrained_decoding_config"] = None
|
|
141
|
+
return original(self, *args, **kwargs)
|
|
142
|
+
|
|
143
|
+
# Not functools.wraps: the marker must survive, and copying __wrapped__
|
|
144
|
+
# would let a second install() unwrap back to the stock method.
|
|
145
|
+
create_conversation.__name__ = getattr(original, "__name__", "create_conversation")
|
|
146
|
+
create_conversation.__doc__ = getattr(original, "__doc__", None)
|
|
147
|
+
setattr(create_conversation, _MARKER, True)
|
|
148
|
+
setattr(engine_cls, "create_conversation", create_conversation)
|
|
149
|
+
return True
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _install_now(module: object) -> None:
|
|
153
|
+
if apply_to(module):
|
|
154
|
+
_say("ACTIVE: unconstrained conversations skip constrained-decoding setup")
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class _LitertLmImportHook:
|
|
158
|
+
"""Applies the wrap immediately after `litert_lm` finishes importing.
|
|
159
|
+
|
|
160
|
+
A `find_spec` that answers only for the target module, hands the real
|
|
161
|
+
machinery back the job of finding it, and decorates the resulting loader's
|
|
162
|
+
`exec_module` so the wrap lands after the module body has run. It removes
|
|
163
|
+
itself first, so it can never recurse and never slows a later import.
|
|
164
|
+
"""
|
|
165
|
+
|
|
166
|
+
def find_spec(self, fullname, path=None, target=None): # noqa: D401
|
|
167
|
+
if fullname != TARGET_MODULE:
|
|
168
|
+
return None
|
|
169
|
+
try:
|
|
170
|
+
sys.meta_path.remove(self)
|
|
171
|
+
except ValueError:
|
|
172
|
+
return None
|
|
173
|
+
try:
|
|
174
|
+
import importlib.util
|
|
175
|
+
|
|
176
|
+
spec = importlib.util.find_spec(fullname)
|
|
177
|
+
if spec is None or spec.loader is None or not hasattr(spec.loader, "exec_module"):
|
|
178
|
+
return None
|
|
179
|
+
real_exec_module = spec.loader.exec_module
|
|
180
|
+
|
|
181
|
+
def exec_module(module):
|
|
182
|
+
real_exec_module(module)
|
|
183
|
+
try:
|
|
184
|
+
_install_now(module)
|
|
185
|
+
except Exception as exc: # noqa: BLE001
|
|
186
|
+
_say(f"NOT ACTIVE: {exc!r} — serving with stock (slower) behaviour")
|
|
187
|
+
|
|
188
|
+
spec.loader.exec_module = exec_module
|
|
189
|
+
return spec
|
|
190
|
+
except Exception as exc: # noqa: BLE001
|
|
191
|
+
_say(f"NOT ACTIVE: {exc!r} — serving with stock (slower) behaviour")
|
|
192
|
+
return None
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def install() -> bool:
|
|
196
|
+
"""Register the import hook. Safe to call more than once. Never raises."""
|
|
197
|
+
try:
|
|
198
|
+
if not _enabled():
|
|
199
|
+
_say("disabled by TRILLBOARDS_LITERTLM_SHIM=0")
|
|
200
|
+
return False
|
|
201
|
+
already = sys.modules.get(TARGET_MODULE)
|
|
202
|
+
if already is not None:
|
|
203
|
+
_install_now(already)
|
|
204
|
+
return True
|
|
205
|
+
if any(isinstance(f, _LitertLmImportHook) for f in sys.meta_path):
|
|
206
|
+
return True
|
|
207
|
+
sys.meta_path.insert(0, _LitertLmImportHook())
|
|
208
|
+
return True
|
|
209
|
+
except Exception as exc: # noqa: BLE001
|
|
210
|
+
_say(f"NOT ACTIVE: {exc!r} — serving with stock (slower) behaviour")
|
|
211
|
+
return False
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
install()
|
|
@@ -6,10 +6,14 @@
|
|
|
6
6
|
#
|
|
7
7
|
# WHY IT EXISTS. Local-only inference makes the box spec the product spec, and
|
|
8
8
|
# every latency number in the capacity plan came off one Apple M4 Max
|
|
9
|
-
# workstation — including the 7.30 s/frame in connectorCadence.ts
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
9
|
+
# workstation — including the 7.30 s/frame in connectorCadence.ts, which is now
|
|
10
|
+
# the BOOTSTRAP a connector uses until it has measured itself (see
|
|
11
|
+
# analyzeCostObserver.ts) rather than a fleet-wide fact. A 3-core / 7 GB Apple
|
|
12
|
+
# silicon runner is the closest CI has to the Mac mini we would actually
|
|
13
|
+
# recommend, and its 7 GB is why it is also the cheapest evidence about memory:
|
|
14
|
+
# the "16 GB+ RAM" minimum this used to point at was RETIRED on 2026-08-18 —
|
|
15
|
+
# cctv-rushi-activation.md now publishes 8 GB, against a measured working set of
|
|
16
|
+
# 2.7 GB stock and 3.31 GB on the shipped GPU-vision path.
|
|
13
17
|
#
|
|
14
18
|
# It does NOT use setup-litertlm.sh's LaunchAgent path: that bootstraps a
|
|
15
19
|
# per-login agent, and a CI runner's session is not the session a customer logs
|
|
@@ -398,6 +398,10 @@ cp "$PKG_ROOT/deploy/macos/com.trillboards.edge.plist" "$PAYLOAD/share/com.trill
|
|
|
398
398
|
# verifies the private service before it opens camera pairing.
|
|
399
399
|
cp "$PKG_ROOT/deploy/macos/com.trillboards.litertlm.plist" "$PAYLOAD/share/com.trillboards.litertlm.plist"
|
|
400
400
|
cp "$PKG_ROOT/deploy/macos/scripts/setup-litertlm.sh" "$PAYLOAD/share/setup-litertlm.sh"
|
|
401
|
+
# Staged NEXT TO setup-litertlm.sh because that is where install_litertlm_shim
|
|
402
|
+
# looks first. It is copied into the litert-lm tool venv's site-packages at
|
|
403
|
+
# provisioning time; nothing else reads it, and no upstream file is modified.
|
|
404
|
+
cp "$PKG_ROOT/deploy/litertlm/trillboards_litertlm_shim.py" "$PAYLOAD/share/trillboards_litertlm_shim.py"
|
|
401
405
|
cp "$PKG_ROOT/deploy/macos/scripts/ensure-litertlm.sh" "$PAYLOAD/share/ensure-litertlm.sh"
|
|
402
406
|
cp "$PKG_ROOT/deploy/macos/apply-update.sh" "$PAYLOAD/share/apply-update.sh"
|
|
403
407
|
chmod +x "$PAYLOAD/share/setup-litertlm.sh" "$PAYLOAD/share/ensure-litertlm.sh" \
|
|
@@ -66,8 +66,23 @@ fail() { log "ERROR: $*"; exit 1; }
|
|
|
66
66
|
# `litert-lm serve` takes NO flags for any of this: `serve` accepts only
|
|
67
67
|
# --host/--port/--api/--cors-origin/--verbose. Every engine knob it honours comes
|
|
68
68
|
# from `$LITERT_LM_DIR/config.json`, which `litert_lm_cli.config.get_model_config`
|
|
69
|
-
# merges per model id. So this file IS the tuning surface
|
|
70
|
-
#
|
|
69
|
+
# merges per model id. So this file IS the tuning surface for everything it can
|
|
70
|
+
# express, and the serve binary stays stock: we pass it no flags and we modify
|
|
71
|
+
# none of its files.
|
|
72
|
+
#
|
|
73
|
+
# ONE knob is NOT expressible there, and it is the largest one left. Every
|
|
74
|
+
# request the serve handles gets `ConstrainedDecodingConfig(enable=True, ...)`
|
|
75
|
+
# built for it unconditionally -- `enable` is True even when the caller sent no
|
|
76
|
+
# `response_format` and the generation is therefore unconstrained -- so the
|
|
77
|
+
# native conversation-create builds constraint machinery for a grammar nothing
|
|
78
|
+
# will use, once per frame. `config_schema.json` on the pinned 0.16.0 has 17
|
|
79
|
+
# keys (backend, cpu_thread_count, audio_backend, vision_backend, cache,
|
|
80
|
+
# max_num_tokens, temperature, top_p, top_k, seed, gpu_decode_steps_per_sync,
|
|
81
|
+
# speculative_decoding, thinking, thinking_budget, activation_data_type, plus
|
|
82
|
+
# the `default`/`models` containers) and NONE of them reaches it; no request
|
|
83
|
+
# field reaches it either, and both 0.15.0 and 0.16.1 construct it identically,
|
|
84
|
+
# so a version move does not reach it. That is why install_litertlm_shim below
|
|
85
|
+
# exists -- see it for the measurement and for why it is not a patch.
|
|
71
86
|
#
|
|
72
87
|
# Two settings, both MEASURED on an M4 Max (12P+4E/48G), litert-lm 0.16.0, Gemma
|
|
73
88
|
# 4 E2B, delegate asserted GPU from the native log, 16 warm-ups before timing,
|
|
@@ -126,6 +141,86 @@ write_litertlm_config() {
|
|
|
126
141
|
log "wrote $CONFIG_JSON (vision_backend=${_vision:-cpu-default}, speculative_decoding=true)"
|
|
127
142
|
}
|
|
128
143
|
|
|
144
|
+
# -- The one knob config.json cannot express -----------------------------------
|
|
145
|
+
#
|
|
146
|
+
# `litert-lm serve` builds `ConstrainedDecodingConfig(enable=True, provider=...)`
|
|
147
|
+
# for EVERY request, at both openai_handler.py construction sites, with `enable`
|
|
148
|
+
# hardcoded True -- so a request that carried no `response_format`, which is
|
|
149
|
+
# every request the connector sends, still makes the native conversation-create
|
|
150
|
+
# build constraint machinery for a grammar nothing will use. It is rebuilt per
|
|
151
|
+
# request because the serve creates one conversation per request.
|
|
152
|
+
#
|
|
153
|
+
# MEASURED (M4 Max 16c/48G, litert-lm 0.16.0, Gemma 4 E2B, GPU/Metal, vision_backend
|
|
154
|
+
# =gpu + speculative_decoding=true, operator/SOP field set, the two committed CCTV
|
|
155
|
+
# fixtures, arms alternated sample-by-sample with the leading arm rotated each
|
|
156
|
+
# round, load average 22-25):
|
|
157
|
+
#
|
|
158
|
+
# in-process, one engine, n=10 pairs
|
|
159
|
+
# arm create_conversation send_message total
|
|
160
|
+
# stock 0.717 s 0.560 s 1.275 s
|
|
161
|
+
# without it 0.001 s 0.603 s 0.604 s
|
|
162
|
+
# paired 2.194x (min 2.040, max 2.284); generations BYTE-IDENTICAL 10/10;
|
|
163
|
+
# prefill 493 / decode 58 on both arms
|
|
164
|
+
#
|
|
165
|
+
# through two stock serves on two ports, interleaved, n=16 pairs
|
|
166
|
+
# 1.309 s -> 0.592 s p50 (p95 3.664 -> 0.965), paired 2.42x (min 2.174, max
|
|
167
|
+
# 4.657), 16/16 schema-valid and 16/16 byte-identical on both arms
|
|
168
|
+
#
|
|
169
|
+
# bench-local-vlm.mjs --arms gpu-sop, 16 warm-ups, n=8, back to back
|
|
170
|
+
# 1.32 s/frame (2,727 analyses/hour) -> 0.72 s/frame (5,000/hour), 8/8
|
|
171
|
+
# schema-valid and 83.3% field accuracy on BOTH arms
|
|
172
|
+
#
|
|
173
|
+
# A speed change, not an answer change -- the same bar speculative_decoding had
|
|
174
|
+
# to clear. The RATIO is itself load-dependent (3.734x at 0.0% CPU idle vs 2.42x
|
|
175
|
+
# at load 25, because the removed work is CPU-bound and the retained work is
|
|
176
|
+
# GPU-bound), so quote the near-idle figure.
|
|
177
|
+
#
|
|
178
|
+
# We do NOT patch upstream. Two files are ADDED to the litert-lm tool venv's
|
|
179
|
+
# site-packages: our shim, and a one-line .pth that imports it. Every upstream
|
|
180
|
+
# file stays byte-identical, no launch definition changes on any platform (so
|
|
181
|
+
# the model OTA's `launchctl kickstart com.trillboards.litertlm` still finds the
|
|
182
|
+
# same label), and a serve started by hand gets the fix too. The shim is lazy --
|
|
183
|
+
# it only registers an import hook, so `litert-lm list`/`--version`, which this
|
|
184
|
+
# script runs as health checks, do not gain a dylib load. It fails OPEN: any
|
|
185
|
+
# failure leaves stock behaviour and prints one line to the serve's own log, and
|
|
186
|
+
# `TRILLBOARDS_LITERTLM_SHIM=0` disables it without uninstalling anything.
|
|
187
|
+
#
|
|
188
|
+
# Runs UNCONDITIONALLY, never inside the install/repair branch: `uv tool install
|
|
189
|
+
# --force` rebuilds the venv and takes both files with it, and the healthy-CLI
|
|
190
|
+
# path skips that branch entirely -- so gating this behind it is exactly how a
|
|
191
|
+
# repaired-and-healthy machine would keep the slow serve while reporting fine.
|
|
192
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd || echo .)"
|
|
193
|
+
find_litertlm_shim() {
|
|
194
|
+
for _c in "$SCRIPT_DIR/trillboards_litertlm_shim.py" \
|
|
195
|
+
"$SCRIPT_DIR/../litertlm/trillboards_litertlm_shim.py" \
|
|
196
|
+
"$SCRIPT_DIR/../../litertlm/trillboards_litertlm_shim.py"; do
|
|
197
|
+
[ -f "$_c" ] && { printf '%s\n' "$_c"; return 0; }
|
|
198
|
+
done
|
|
199
|
+
return 1
|
|
200
|
+
}
|
|
201
|
+
install_litertlm_shim() {
|
|
202
|
+
_shim="$(find_litertlm_shim)" || { log "WARN: litert-lm shim source not found; serve keeps stock (slower) conversation setup"; return 1; }
|
|
203
|
+
_tooldir="$("$UV" tool dir 2>/dev/null | tr -d '\r')"
|
|
204
|
+
[ -n "$_tooldir" ] || { log "WARN: could not resolve \`uv tool dir\`; skipping litert-lm shim"; return 1; }
|
|
205
|
+
_py="$_tooldir/litert-lm/bin/python"
|
|
206
|
+
[ -x "$_py" ] || { log "WARN: litert-lm venv python missing at $_py; skipping shim"; return 1; }
|
|
207
|
+
_site="$("$_py" -c 'import sysconfig;print(sysconfig.get_paths()["purelib"])' 2>/dev/null | tr -d '\r')"
|
|
208
|
+
[ -n "$_site" ] && [ -d "$_site" ] || { log "WARN: could not resolve litert-lm site-packages; skipping shim"; return 1; }
|
|
209
|
+
cp -f "$_shim" "$_site/trillboards_litertlm_shim.py" 2>/dev/null \
|
|
210
|
+
|| { log "WARN: could not copy the litert-lm shim into $_site"; return 1; }
|
|
211
|
+
printf 'import trillboards_litertlm_shim\n' > "$_site/trillboards-litertlm-shim.pth" 2>/dev/null \
|
|
212
|
+
|| { log "WARN: could not write the litert-lm shim .pth"; return 1; }
|
|
213
|
+
# Capability is established by ATTEMPTING THE OPERATION. Importing litert_lm
|
|
214
|
+
# here is the only thing that proves the wrap actually landed on the class the
|
|
215
|
+
# serve will use; a file that merely exists proves nothing.
|
|
216
|
+
if "$_py" -c 'import litert_lm,sys; sys.exit(0 if getattr(litert_lm.Engine.create_conversation,"_trillboards_litertlm_shim",False) else 1)' >>"$LOG" 2>&1; then
|
|
217
|
+
log "litert-lm fast-conversation shim active in $_site"
|
|
218
|
+
return 0
|
|
219
|
+
fi
|
|
220
|
+
log "WARN: litert-lm shim did not take effect; serve keeps stock (slower) conversation setup"
|
|
221
|
+
return 1
|
|
222
|
+
}
|
|
223
|
+
|
|
129
224
|
# One real image inference against the running serve. A 200 means the vision
|
|
130
225
|
# encoder loaded AND ran on whatever backend config.json just asked for.
|
|
131
226
|
# Deliberately NOT a /v1/models check: that answers 200 without ever building an
|
|
@@ -332,6 +427,8 @@ else
|
|
|
332
427
|
log "litert-lm CLI repaired successfully at $("$LITERTLM" --version 2>/dev/null | awk '{print $NF}')"
|
|
333
428
|
fi
|
|
334
429
|
|
|
430
|
+
install_litertlm_shim || true
|
|
431
|
+
|
|
335
432
|
# 3. Model - import the verified bundled file. Idempotent: skip entirely if
|
|
336
433
|
# litert-lm already has the model. Engineering runs may use HF only with the
|
|
337
434
|
# explicit TRILLBOARDS_ALLOW_MODEL_DOWNLOAD=1 opt-in.
|
|
@@ -74,8 +74,23 @@ fail() { log "ERROR: $*"; exit 1; }
|
|
|
74
74
|
# `litert-lm serve` takes NO flags for any of this: `serve` accepts only
|
|
75
75
|
# --host/--port/--api/--cors-origin/--verbose. Every engine knob it honours comes
|
|
76
76
|
# from `$LITERT_LM_DIR/config.json`, which `litert_lm_cli.config.get_model_config`
|
|
77
|
-
# merges per model id. So this file IS the tuning surface
|
|
78
|
-
#
|
|
77
|
+
# merges per model id. So this file IS the tuning surface for everything it can
|
|
78
|
+
# express, and the serve binary stays stock: we pass it no flags and we modify
|
|
79
|
+
# none of its files.
|
|
80
|
+
#
|
|
81
|
+
# ONE knob is NOT expressible there, and it is the largest one left. Every
|
|
82
|
+
# request the serve handles gets `ConstrainedDecodingConfig(enable=True, ...)`
|
|
83
|
+
# built for it unconditionally -- `enable` is True even when the caller sent no
|
|
84
|
+
# `response_format` and the generation is therefore unconstrained -- so the
|
|
85
|
+
# native conversation-create builds constraint machinery for a grammar nothing
|
|
86
|
+
# will use, once per frame. `config_schema.json` on the pinned 0.16.0 has 17
|
|
87
|
+
# keys (backend, cpu_thread_count, audio_backend, vision_backend, cache,
|
|
88
|
+
# max_num_tokens, temperature, top_p, top_k, seed, gpu_decode_steps_per_sync,
|
|
89
|
+
# speculative_decoding, thinking, thinking_budget, activation_data_type, plus
|
|
90
|
+
# the `default`/`models` containers) and NONE of them reaches it; no request
|
|
91
|
+
# field reaches it either, and both 0.15.0 and 0.16.1 construct it identically,
|
|
92
|
+
# so a version move does not reach it. That is why install_litertlm_shim below
|
|
93
|
+
# exists -- see it for the measurement and for why it is not a patch.
|
|
79
94
|
#
|
|
80
95
|
# Two settings, both MEASURED on an M4 Max (12P+4E/48G), litert-lm 0.16.0, Gemma
|
|
81
96
|
# 4 E2B, delegate asserted GPU from the native log, 16 warm-ups before timing,
|
|
@@ -134,6 +149,86 @@ write_litertlm_config() {
|
|
|
134
149
|
log "wrote $CONFIG_JSON (vision_backend=${_vision:-cpu-default}, speculative_decoding=true)"
|
|
135
150
|
}
|
|
136
151
|
|
|
152
|
+
# -- The one knob config.json cannot express -----------------------------------
|
|
153
|
+
#
|
|
154
|
+
# `litert-lm serve` builds `ConstrainedDecodingConfig(enable=True, provider=...)`
|
|
155
|
+
# for EVERY request, at both openai_handler.py construction sites, with `enable`
|
|
156
|
+
# hardcoded True -- so a request that carried no `response_format`, which is
|
|
157
|
+
# every request the connector sends, still makes the native conversation-create
|
|
158
|
+
# build constraint machinery for a grammar nothing will use. It is rebuilt per
|
|
159
|
+
# request because the serve creates one conversation per request.
|
|
160
|
+
#
|
|
161
|
+
# MEASURED (M4 Max 16c/48G, litert-lm 0.16.0, Gemma 4 E2B, GPU/Metal, vision_backend
|
|
162
|
+
# =gpu + speculative_decoding=true, operator/SOP field set, the two committed CCTV
|
|
163
|
+
# fixtures, arms alternated sample-by-sample with the leading arm rotated each
|
|
164
|
+
# round, load average 22-25):
|
|
165
|
+
#
|
|
166
|
+
# in-process, one engine, n=10 pairs
|
|
167
|
+
# arm create_conversation send_message total
|
|
168
|
+
# stock 0.717 s 0.560 s 1.275 s
|
|
169
|
+
# without it 0.001 s 0.603 s 0.604 s
|
|
170
|
+
# paired 2.194x (min 2.040, max 2.284); generations BYTE-IDENTICAL 10/10;
|
|
171
|
+
# prefill 493 / decode 58 on both arms
|
|
172
|
+
#
|
|
173
|
+
# through two stock serves on two ports, interleaved, n=16 pairs
|
|
174
|
+
# 1.309 s -> 0.592 s p50 (p95 3.664 -> 0.965), paired 2.42x (min 2.174, max
|
|
175
|
+
# 4.657), 16/16 schema-valid and 16/16 byte-identical on both arms
|
|
176
|
+
#
|
|
177
|
+
# bench-local-vlm.mjs --arms gpu-sop, 16 warm-ups, n=8, back to back
|
|
178
|
+
# 1.32 s/frame (2,727 analyses/hour) -> 0.72 s/frame (5,000/hour), 8/8
|
|
179
|
+
# schema-valid and 83.3% field accuracy on BOTH arms
|
|
180
|
+
#
|
|
181
|
+
# A speed change, not an answer change -- the same bar speculative_decoding had
|
|
182
|
+
# to clear. The RATIO is itself load-dependent (3.734x at 0.0% CPU idle vs 2.42x
|
|
183
|
+
# at load 25, because the removed work is CPU-bound and the retained work is
|
|
184
|
+
# GPU-bound), so quote the near-idle figure.
|
|
185
|
+
#
|
|
186
|
+
# We do NOT patch upstream. Two files are ADDED to the litert-lm tool venv's
|
|
187
|
+
# site-packages: our shim, and a one-line .pth that imports it. Every upstream
|
|
188
|
+
# file stays byte-identical, no launch definition changes on any platform (the
|
|
189
|
+
# systemd unit's ExecStart is untouched), and a serve started by hand gets the
|
|
190
|
+
# fix too. The shim is lazy --
|
|
191
|
+
# it only registers an import hook, so `litert-lm list`/`--version`, which this
|
|
192
|
+
# script runs as health checks, do not gain a dylib load. It fails OPEN: any
|
|
193
|
+
# failure leaves stock behaviour and prints one line to the serve's own log, and
|
|
194
|
+
# `TRILLBOARDS_LITERTLM_SHIM=0` disables it without uninstalling anything.
|
|
195
|
+
#
|
|
196
|
+
# Runs UNCONDITIONALLY, never inside the install/repair branch: `uv tool install
|
|
197
|
+
# --force` rebuilds the venv and takes both files with it, and the healthy-CLI
|
|
198
|
+
# path skips that branch entirely -- so gating this behind it is exactly how a
|
|
199
|
+
# repaired-and-healthy machine would keep the slow serve while reporting fine.
|
|
200
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd || echo .)"
|
|
201
|
+
find_litertlm_shim() {
|
|
202
|
+
for _c in "$SCRIPT_DIR/trillboards_litertlm_shim.py" \
|
|
203
|
+
"$SCRIPT_DIR/../litertlm/trillboards_litertlm_shim.py" \
|
|
204
|
+
"$SCRIPT_DIR/../../litertlm/trillboards_litertlm_shim.py"; do
|
|
205
|
+
[ -f "$_c" ] && { printf '%s\n' "$_c"; return 0; }
|
|
206
|
+
done
|
|
207
|
+
return 1
|
|
208
|
+
}
|
|
209
|
+
install_litertlm_shim() {
|
|
210
|
+
_shim="$(find_litertlm_shim)" || { log "WARN: litert-lm shim source not found; serve keeps stock (slower) conversation setup"; return 1; }
|
|
211
|
+
_tooldir="$("$UV" tool dir 2>/dev/null | tr -d '\r')"
|
|
212
|
+
[ -n "$_tooldir" ] || { log "WARN: could not resolve \`uv tool dir\`; skipping litert-lm shim"; return 1; }
|
|
213
|
+
_py="$_tooldir/litert-lm/bin/python"
|
|
214
|
+
[ -x "$_py" ] || { log "WARN: litert-lm venv python missing at $_py; skipping shim"; return 1; }
|
|
215
|
+
_site="$("$_py" -c 'import sysconfig;print(sysconfig.get_paths()["purelib"])' 2>/dev/null | tr -d '\r')"
|
|
216
|
+
[ -n "$_site" ] && [ -d "$_site" ] || { log "WARN: could not resolve litert-lm site-packages; skipping shim"; return 1; }
|
|
217
|
+
cp -f "$_shim" "$_site/trillboards_litertlm_shim.py" 2>/dev/null \
|
|
218
|
+
|| { log "WARN: could not copy the litert-lm shim into $_site"; return 1; }
|
|
219
|
+
printf 'import trillboards_litertlm_shim\n' > "$_site/trillboards-litertlm-shim.pth" 2>/dev/null \
|
|
220
|
+
|| { log "WARN: could not write the litert-lm shim .pth"; return 1; }
|
|
221
|
+
# Capability is established by ATTEMPTING THE OPERATION. Importing litert_lm
|
|
222
|
+
# here is the only thing that proves the wrap actually landed on the class the
|
|
223
|
+
# serve will use; a file that merely exists proves nothing.
|
|
224
|
+
if "$_py" -c 'import litert_lm,sys; sys.exit(0 if getattr(litert_lm.Engine.create_conversation,"_trillboards_litertlm_shim",False) else 1)' >>"$LOG" 2>&1; then
|
|
225
|
+
log "litert-lm fast-conversation shim active in $_site"
|
|
226
|
+
return 0
|
|
227
|
+
fi
|
|
228
|
+
log "WARN: litert-lm shim did not take effect; serve keeps stock (slower) conversation setup"
|
|
229
|
+
return 1
|
|
230
|
+
}
|
|
231
|
+
|
|
137
232
|
# One real image inference against the running serve. A 200 means the vision
|
|
138
233
|
# encoder loaded AND ran on whatever backend config.json just asked for.
|
|
139
234
|
# Deliberately NOT a /v1/models check: that answers 200 without ever building an
|
|
@@ -226,6 +321,8 @@ else
|
|
|
226
321
|
log "litert-lm CLI repaired at $("$LITERTLM" --version 2>/dev/null | awk '{print $NF}')"
|
|
227
322
|
fi
|
|
228
323
|
|
|
324
|
+
install_litertlm_shim || true
|
|
325
|
+
|
|
229
326
|
# 3. Model. Idempotent: skip entirely if litert-lm already has it.
|
|
230
327
|
HAVE_MODEL=0
|
|
231
328
|
if "$LITERTLM" list 2>/dev/null | grep -q "$MODEL_ID"; then
|
|
@@ -222,6 +222,10 @@ Log "Customer setup window smoke test passed"
|
|
|
222
222
|
# model path from the customer SFX and verifies local AI before pairing; the
|
|
223
223
|
# serve task keeps `litert-lm serve :9379` alive for local-sense.
|
|
224
224
|
Copy-Item -Force "$Here\setup-litertlm.ps1" "$Payload\bin\setup-litertlm.ps1"
|
|
225
|
+
# Staged NEXT TO setup-litertlm.ps1 because that is where Install-LitertlmShim
|
|
226
|
+
# looks first. Copied into the litert-lm tool venv's site-packages at
|
|
227
|
+
# provisioning time; nothing else reads it, and no upstream file is modified.
|
|
228
|
+
Copy-Item -Force "$Here\..\litertlm\trillboards_litertlm_shim.py" "$Payload\bin\trillboards_litertlm_shim.py"
|
|
225
229
|
Copy-Item -Force "$Here\register-litertlm-task.ps1" "$Payload\bin\register-litertlm-task.ps1"
|
|
226
230
|
Copy-Item -Force "$Here\check-feeds.js" "$Payload\bin\check-feeds.js"
|
|
227
231
|
|