feat: an optional lane says it is optional, and what enabling it costs (4296)
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 2s
Build images / build-agent (push) Successful in 5s
CI / extension-version (push) Successful in 2s
CI / frontend-build (push) Successful in 23s
CI / backend-lint-and-test (push) Successful in 31s
Build images / build-web (push) Successful in 1m21s
Build images / smoke-web (push) Skipped
Build images / build-ml (push) Successful in 2m12s
Build images / promote (push) Skipped
CI / integration (push) Successful in 2m39s

Operator, 2026-09-22: "since the ml-worker is optional it should be shown as
such in the UI and have a warning about what it does and that it pulls the
models and what models and their projected size and ram requirements to run."

The card previously said "a few GB, once" — a number sourced from nothing,
which is exactly the hand-wave I had flagged in this step's own survey log as
something that should be measured rather than asserted.

ONE FACT CORRECTED WHILE WRITING THE COPY. I had named the lane "ML tagging".
It downloads an EMBEDDER: google/siglip-so400m-patch14-384. WD14 tagging is
the GPU agent's job — celery_app.py:5 still names both, but that has been
stale since B3 (#1238), when the agent took over and this lane was left as
the CPU embed fallback for stacks running no agent (see
MLSettings.cpu_embed_enabled). Telling someone the lane "does tagging" would
have been wrong in exactly the way this request exists to prevent.

The facts are structured data on the lane, not prose in a component:
ModelRequirement(repo, approx_download_bytes, approx_resident_bytes,
measured). The API carries them; the card renders them. Numbers come from the
system, wording from the UI.

ML_BYTES_PER_SLOT IS NOW DERIVED from that requirement rather than stated
separately. They have to be one number: the figure quoted to the operator
before they enable the lane and the figure the cap enforces. Two copies could
disagree, and the UI would promise a slot the cap then refuses.

`measured=False` travels with the numbers and the card renders "about". They
are estimates from the checkpoint's parameter count and dtype — ~877M params
at fp32 is ~3.5GB of weights — not from a build. This decides whether
someone's server survives, so it is labelled rather than rounded into
something that reads like a fact. A test asserts the flag is false, to be
flipped in the same commit that records a real measurement.

The card now shows: an "optional" chip in the row itself (someone scanning
the table should not have to enable a lane to learn it was never required),
and before the switch, what the lane does, that you only need it if you are
NOT running the GPU agent, the repo id, the download size, the per-slot RAM,
and why the ceiling is what it is — including saying plainly when a box has
too little memory to run it at all.

Keyed on the lane's own `optional` flag, not on the name 'ml', so a second
optional lane gets the same treatment without anyone remembering to add it.
A test asserts no REQUIRED lane declares a model: if one ever needs a
download, it stops being required.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LVjrnpQjRgHdvq95rASoiR
This commit is contained in:
2026-09-22 08:45:22 -04:00
co-authored by Claude Opus 5
parent e579333455
commit ecbd325437
4 changed files with 181 additions and 21 deletions
@@ -43,7 +43,15 @@
<tbody>
<tr v-for="lane in lanesList" :key="lane.name">
<td>
<div>{{ lane.display_name }}</div>
<div class="d-flex align-center" style="gap: 6px;">
<span>{{ lane.display_name }}</span>
<!-- Said in the row, not only in the warning below. Someone
scanning the table to work out why a lane is off should
not have to enable it to find out it was never required. -->
<v-chip v-if="lane.optional" size="x-small" variant="tonal">
optional
</v-chip>
</div>
<!-- Not present is NOT zero slots it is "nothing answered".
Saying "stopped" here would be a verdict drawn from an
unswept read, and the operator would go looking for a crash
@@ -111,10 +119,42 @@
</tbody>
</v-table>
<p v-if="mlOff" class="fc-section__hint mt-3">
ML tagging is off. Turning it on downloads the tagging model the first
time it runs a few GB, once.
</p>
<!-- What an optional, off lane will cost BEFORE it is switched on.
Every number here comes from the lane payload, and `measured`
travels with them an estimate is labelled rather than rounded
into something that reads like a fact. The previous version of this
said "a few GB", which told the operator nothing they could plan
with and was not sourced from anything. -->
<v-alert
v-for="lane in offOptionalLanes" :key="`advisory-${lane.name}`"
type="info" variant="tonal" density="compact" class="mt-3"
>
<div class="font-weight-medium mb-1">
{{ lane.display_name }} is optional and currently off
</div>
<p class="mb-2">
It computes image embeddings on the CPU what similarity search,
duplicate grouping and tag suggestions are built on. You only need it
if you are <em>not</em> running the GPU agent, which does the same
work faster.
</p>
<p class="mb-1">Turning it on downloads, once:</p>
<ul class="mb-2">
<li v-for="m in lane.models" :key="m.repo">
<code>{{ m.repo }}</code>
{{ approx(m.measured) }}{{ gb(m.download_bytes) }} to download,
and about {{ gb(m.resident_bytes) }} of RAM for
<strong>each</strong> slot while it runs.
</li>
</ul>
<p class="mb-0 text-caption">
Each slot loads its own copy, which is why this container caps the
lane at {{ lane.ceiling }}.
<template v-if="lane.ceiling === 0">
It has too little memory to run this at all.
</template>
</p>
</v-alert>
</v-card-text>
</v-card>
</template>
@@ -134,10 +174,25 @@ const busy = ref(null)
const notice = ref(null)
const lanesList = computed(() => store.lanes?.lanes ?? [])
const mlOff = computed(() =>
lanesList.value.some((l) => l.name === 'ml' && !l.enabled),
// Optional lanes that are OFF — the only ones whose cost the operator has not
// already accepted. Keyed on the lane's own `optional` flag rather than on the
// name 'ml', so a second optional lane gets the same treatment without anyone
// remembering to add it here.
const offOptionalLanes = computed(() =>
lanesList.value.filter((l) => l.optional && !l.enabled && l.models?.length),
)
function gb(bytes) {
return `${(bytes / 1024 ** 3).toFixed(1)} GB`
}
// An unmeasured figure says so. It decides whether someone's server survives,
// and presenting an estimate as a measurement is the failure this guards.
function approx(measured) {
return measured ? '' : 'about '
}
async function apply(lane, fields) {
busy.value = lane.name
notice.value = null