feat(inference): connections declare what their model can be asked for - #486
Merged
Conversation
A connection row said where a model runs and whether its weights had arrived, and nothing anywhere said what it could be asked to do. The family is already resolved from the model's own config on every provider build, used to pick an adapter, and then thrown away — so no surface could publish it and no client could filter on it. - `InferenceConnection` records `model_family`, written when it becomes knowable: at download completion, from the config that arrived with the weights. Three states, and the third earns its place — NULL is "nobody has looked", "" is "looked and it declared nothing", so a look that found nothing is not repeated on every read. - `families.py` holds the family sets and the family-to-capability map, derived from those sets rather than listed again: an adapter and its declaration are now one edit. - `ConnectionOut` and `wire.connection` publish `capabilities`, empty wherever nothing is known — including an `http` connection, which declares nothing until the remote contract says how an endpoint states what it can do. - Migration 7 adds the column. It cannot backfill: the answer lives in a model cache the kernel is forbidden to reach. So the read path fills it in once per pre-existing row, from files already on this disk, and a build without the optional runtime records nothing rather than recording that it found nothing. - Editing a connection's model or revision forgets the family. A stale answer about the previous weights reads exactly like a fresh one.
…ets you choose `usableConnection` was `find(row => row.setup_state === "ready")`. With a text-prompt detector as the only ready connection, every click round-tripped and came back with a truthful `UnsupportedPrompt` refusal — the tool was offered where it could never work, one click at a time. - Candidates are the ready connections declaring `point_suggest`. The declaration comes off the wire; nothing here re-derives it from a model id. - A fourth blocker says so when connections are ready and none can answer, ranked *below* `not-ready` — an undownloaded connection has no capability yet, and "wrong kind of model" would be the wrong sentence for one whose weights simply have not arrived. - Where more than one can answer, the idle card carries a picker, remembered per project in `prefs`. One candidate renders the line naming it and no control; a remembered choice that is no longer a candidate falls back rather than blocking the tool over a connection somebody deleted. - The picker is on the idle card only. Changing which model answers while an answer is on screen would leave a proposal nothing on the card explains. The request still carries an explicit `connection_id` and the server still refuses a wrong-family one. The filter is convenience; the kernel is the law.
- `inference.md` gains the capability vocabulary, where the value comes from (the model's own config, never its name), what empty means and when it is recorded — plus the editor case that reads oddly without it: a workspace can have a connection configured, downloaded and running and still have nothing that can answer a click. - `api.md` separates the two declarations. An action is something to do *to* a resource and follows its state; a capability is what its model answers and follows the weights. Offering a tool needs both. - `ui.md` describes the suggest tool's connection: the narrower candidate set, the picker where there is a choice, and why the choice is per project and per browser rather than a workspace setting. - `persistence.md`'s migration list was two generations behind. Correcting it also corrects the table-creating exception, which is migrations 4 *and* 6, and records the rule migration 7 is the first case of: where a value is unknowable inside the kernel, the column arrives NULL and says who fills it in. - `architecture/backend/inference.md` records why the family sets and the capability map share a module, and why the backfill is on the read path. - `architecture/backend/wire.md` names its one non-kernel import.
JArmandoAnaya
force-pushed
the
feat/connection-capability
branch
from
August 10, 2026 04:29
47cb435 to
9477f07
Compare
…out it CI runs without the `local-inference` extra, so the config read that follows a download raised `LocalInferenceUnavailable` — and took the download with it. The connection landed back at `not_set_up` beside a full cache, offering as its remedy the transfer that had just succeeded. Fifteen tests, all of them downloading through a faked `download` into the real config read. Not knowing is recoverable and is now what gets recorded: `None` rather than a refusal, so the next read of the connection asks again. It is the same rule `with_families` already followed for a build that cannot look, and both now go through one function so there is a single answer to what such a build records.
This was referenced Aug 10, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A connection row said where a model runs and whether its weights are here, and
nothing anywhere said what it can be asked to do. So the editor picked by the
only property it could see —
find(row => row.setup_state === "ready")— and witha text-prompt detector as the only ready connection, every
Sclick round-trippedand came back with a truthful
UnsupportedPromptrefusal. A tool offered where itcould never work, one refusal at a time.
Three commits: the kernel/wire half, the editor half, the docs.
Naming
capabilities, a list onConnectionOutallowed_actions' grammar — a list of a closed enum, empty rather than null. "Empty" was the decision's own word for the unknown case, and a list is what makes that spelling exist.ModelCapability.POINT_SUGGEST/TEXT_DETECTSuggestBlockergains"not-capable""not-ready"'s shape, and it reads beside it.visionset.inference.familiesThe mapping is derived, not written
CAPABILITY_BY_FAMILYis built fromSEGMENTER_FAMILIESandDETECTOR_FAMILIESrather than listed beside them. A hand-written map lets a family be added to a set,
get an adapter, ship, and declare nothing — the model runs and no client offers it,
which is invisible from both ends. Derived, an adapter and its declaration are one
edit, and
test_the_mapping_covers_exactly_what_this_build_can_runsays so.The vocabulary is the kernel's and the mapping is not, deliberately: what a tool can
ask for is a domain word, while which
model_typevalues this build serves is afact about an optional runtime the kernel has no view of.
Step 0, before designing: where the resolver runs
The halt condition was "the resolver cannot run where the design assumes". It can.
family_ofreads the model's own config out of the workspace cache withlocal_files_only, so at download completion the file is already there and nonetwork is touched.
fetch_weights— the one sequence the job, the CLI and a futureUI all run — now ends with that read and records what it found. Nothing resolves at
connection creation, which keeps the form's promise intact.
It moved from
providers.pyintofamilies.pyverbatim.weights.pyneeds it andproviders.pyimportsweights.cache_root, so calling it in place would have closedan import cycle. One resolver, one spelling, one module — which is also where the
capability map wanted to live.
Three states, not two.
model_familyisNULLfor nobody has looked,""forlooked and the config declared nothing, and a family otherwise.
""is the answerfamily_ofalready gave an unreadable config; keeping it distinct is what stops alook that found nothing from being repeated on every read for the life of the
workspace.
The backfill is on the read path, and that is a layering fact
A migration is the natural home and cannot be one: the answer lives in a model cache
under the workspace, migrations run inside the kernel, and the kernel may not import
visionset.inference(contract 1) nor address that cache. Sowith_familiesfills arow in on the first read of it — bounded to
local+ready+ never-asked, onesmall JSON read, no network, and never again once the row has any answer. Rows
created from here on arrive with a family from the download and never enter the loop.
A build without the optional runtime records nothing rather than recording that it
found nothing: a build that cannot look has not looked, and writing
""there wouldlet a machine that later installs the runtime go on believing an answer nobody
produced.
Precedence
checking→no-connections→not-ready→not-capable→ usable.not-readyoutranksnot-capableand the ordering is load-bearing: an undownloadedconnection has no capability yet, because nothing has read its config. Ranking
capability first would tell somebody their SAM connection answers the wrong question
when the truth is that its weights have not arrived. #473's parked-class precedence
is untouched and still outranks every blocker — a class that can hold no shape is
not a fact about connections.
Two things the dispatch assumed, checked
frontend/ui-core/src/data/prefs.ts(
readPref/writePref, namespacedvisionset.prefs., with the storage-throws guardand an in-memory fallback). The key is
suggest.connection.<projectId>. Nothing newwas invented.
it "stays"; a grep over the annotator finds nothing that displays a connection's name
or model. I added one, because the single-candidate case is otherwise the only path
where the panel never says what is answering — it is the same line the picker
replaces once there is a choice.
Bounds
No batch-mode surface:
text_detectis declared and has no tool (cf. #425). No HTTPadapter work — an
httpconnection declares nothing, and how a remote endpoint stateswhat it can do is that slice's inherited question (
cf. #418). The suggest route'scontract is unchanged and it still refuses a wrong-family request; the client filter is
convenience, the kernel is the law.
providers.pychanged by having the family sets andfamily_ofmoved out of it, withno behavioural edit — I read "no changes to providers" as no change to how a provider
is resolved, since the same decision explicitly asks for the mapping to sit beside
those sets. MCP has no inference surface at HEAD and gains none.
Mutation verification
Each applied to a committed tree, reverted by its exact diff, anchor asserted before
and after.
candidates = ready)fetch_weightsstops passingmodel_familytests/inference/test_weights.py. The server capability test survives, and correctly: the read-path backfill covers for the missing write. The download-time write is guarded by the inference tests, not the wire ones.wire.connectionpublishes the key under another nameConnectionOut.capabilitieshardcoded to[]CAPABILITY_BY_FAMILYhand-written, missingsam2_videonot-capableranked abovenot-readyFound, not fixed
readylocal connection staysreadywhen itsmodel_idis edited, though thenew model's weights are not on the machine. Pre-existing. This change makes the row
forget its family on such an edit — a stale capability is worse than none — but the
setup state is out of scope here.
test_configuring_a_connection_reaches_no_model_runtimeasserts a process-globalsys.modulesfact, so it is order-dependent across the whole suite. It stays true,and three fixtures that fake a download now also fake the config read to keep it that
way, each saying why. Worth narrowing to the claim its own docstring makes.
Rebased onto
5f2c61cFour PRs landed mid-flight (#478, #479, #483, #484). One conflict, in
AnnotationPage.tsx: #484 moved every floating editor message — the suggest panelincluded — into one top-right
EditorNoticescolumn. Resolved by keeping thatstructure and passing the three new props to the
SuggestPanelinside it; thepanel chooses the sentence and #484 owns the geometry, which is the split its own
docstring states. The whole tree was re-linted and the entire gate re-run below on
the rebased tip, with
uv syncandpnpm installfor the two lockfile bumps.CI caught one the local gate could not (attempt 1 → fixed)
pythoncame back with 15 failures and they were a real defect, not a testproblem. CI runs without the
local-inferenceextra; my worktree had itsynced. So the config read that now follows a download raised
LocalInferenceUnavailable— and took the download with it. The connection landedback at
not_set_upbeside a full cache, offering as its remedy the transfer thathad just succeeded.
A download that worked must not be undone by a question about it.
Noneisrecorded instead — not knowing is recoverable, and the next read of the
connection asks again — and
with_familiesalready followed exactly that rule fora build that cannot look, so both now go through one function.
Reproduced and re-verified the way CI has it:
uv syncwith no extras(
transformersandhuggingface_hubgenuinely absent), then the whole suite inone process — 3081 passed, 9 skipped, exit 0. That single-process run is also
what exercises the
sys.modulesimport-purity guard across directories.Test plan
Full local gate, staged (harness ceiling), every stage exit 0. The pytest stages
below ran with the optional runtime present; the whole-suite run without it is
the one above, and it is the one that matches CI.
pytest tests/architecturepytest tests/clipytest tests/examplespytest tests/formatspytest tests/inferencepytest tests/jobspytest tests/kernelpytest tests/mcppytest tests/packagingpytest tests/serverpytest tests/test_versioning.pyruff check ./ruff format --check .mypy src/visionsetlint-importspnpm -r build/pnpm -r lintpnpm testscripts/check.sh generatedscripts/check.sh browseropenapi.jsonandfrontend/ui-core/src/generated/regenerated and committed. Loadaverage 1.9 on 20 cores — the full gate, not the saturation fallback.
New coverage: the family read (config over model id, unreadable config, no runtime),
the capability derivation and its three empty cases, the download-time write and the
re-download that repairs a missing one, the backfill and its bounds, the wire
declaration per kind and state, the backfill over HTTP with its read count, the
capability filter and the blocker's precedence, the picker's presence and absence, and
the per-project memory including that one project's choice does not reach another.
The reproduction is a test: a DINO-only workspace shows the blocker and sends no
request — asserted as the absence of the call, not as the shape of a refusal.
Closes #480