Summary
Upscaling is dispatched two different ways, with two different guards, and neither is a capability check. The result is that the same input is accepted on one path and rejected on the other.
The direct endpoint doesn't check the upscaling label
Server::handle_image_upscale resolves the model, reads its recipe, and dispatches — but never checks that the model is actually an upscaler. Verified at runtime against a local build:
POST /v1/images/upscale {"model": "SD-Turbo", ...}
SD-Turbo is a generation model labelled image, not upscaling. It is accepted and handed to sd-cli as though it were an upscaler. (It failed later only because the sd-cpp backend wasn't installed in that cache directory — the label never entered into it.)
This is pre-existing behavior, not introduced by #3327.
The auto-upscale path does check the label, but not the recipe
#2842 adds Server::do_upscale, which gates on has_label(info.labels, "upscaling") and then hardcodes try_get_spec_for_recipe("sd-cpp") and SDServer::upscale_via_cli. So it enforces the label the endpoint ignores, and ignores the recipe the endpoint dispatches on.
Once both land, the two paths disagree in both directions:
| Input |
/v1/images/upscale |
auto-upscale (upscale_model) |
sd-cpp generation model (no upscaling label) |
accepted, sent to sd-cli |
rejected (400) |
TheNoise upscaler (upscaling label, thenoise recipe) |
dispatched correctly |
accepted, sent to sd-cli — wrong binary |
The second row is the harmful one: #3327 adds five recipe: "thenoise" upscalers, all suggested: true, so it is the natural thing for a user to select.
Proposal
Replace both ad-hoc guards with a capability interface alongside the others in src/cpp/include/lemon/server_capabilities.h — e.g. IUpscaleServer, with supports_capability<IUpscaleServer>(server) as the single gate. That gives one place that answers "can this model upscale, and who does it", rather than a label check in one path and a recipe if/else in the other.
Concretely:
- Add the capability interface and implement it on
SDServer and TheNoiseServer.
- Replace the
if (recipe == "thenoise") ... else if (recipe == "sd-cpp") chain in handle_image_upscale with a capability lookup.
- Have
do_upscale use the same lookup instead of hardcoding sd-cpp.
- Keep the
upscaling label as the discovery filter (what the UI lists), but make the capability the dispatch gate.
Context
Found while reviewing #3327 and #2842 together, and confirmed against a local build (ctest -L cpp-ci: 55/55 passing at #3327's head). Not blocking either PR — #3327's recipe dispatch is a strict improvement over what main does today, and #2842 has been asked to route through it. This issue is the cleanup that makes the split unnecessary.
Summary
Upscaling is dispatched two different ways, with two different guards, and neither is a capability check. The result is that the same input is accepted on one path and rejected on the other.
The direct endpoint doesn't check the
upscalinglabelServer::handle_image_upscaleresolves the model, reads its recipe, and dispatches — but never checks that the model is actually an upscaler. Verified at runtime against a local build:SD-Turbois a generation model labelledimage, notupscaling. It is accepted and handed tosd-clias though it were an upscaler. (It failed later only because the sd-cpp backend wasn't installed in that cache directory — the label never entered into it.)This is pre-existing behavior, not introduced by #3327.
The auto-upscale path does check the label, but not the recipe
#2842 adds
Server::do_upscale, which gates onhas_label(info.labels, "upscaling")and then hardcodestry_get_spec_for_recipe("sd-cpp")andSDServer::upscale_via_cli. So it enforces the label the endpoint ignores, and ignores the recipe the endpoint dispatches on.Once both land, the two paths disagree in both directions:
/v1/images/upscaleupscale_model)upscalinglabel)upscalinglabel,thenoiserecipe)The second row is the harmful one: #3327 adds five
recipe: "thenoise"upscalers, allsuggested: true, so it is the natural thing for a user to select.Proposal
Replace both ad-hoc guards with a capability interface alongside the others in
src/cpp/include/lemon/server_capabilities.h— e.g.IUpscaleServer, withsupports_capability<IUpscaleServer>(server)as the single gate. That gives one place that answers "can this model upscale, and who does it", rather than a label check in one path and a recipeif/elsein the other.Concretely:
SDServerandTheNoiseServer.if (recipe == "thenoise") ... else if (recipe == "sd-cpp")chain inhandle_image_upscalewith a capability lookup.do_upscaleuse the same lookup instead of hardcoding sd-cpp.upscalinglabel as the discovery filter (what the UI lists), but make the capability the dispatch gate.Context
Found while reviewing #3327 and #2842 together, and confirmed against a local build (
ctest -L cpp-ci: 55/55 passing at #3327's head). Not blocking either PR — #3327's recipe dispatch is a strict improvement over whatmaindoes today, and #2842 has been asked to route through it. This issue is the cleanup that makes the split unnecessary.