Fix stable-diffusion.cpp model routing and URL path handling (#5427) * fix: include model field in sd.cpp SDAPI requests and preserve URL path The sd.cpp integration overwrites the URL pathname when constructing requests, which breaks proxy servers like llama-swap that use path-based routing (e.g. /upstream/model-name). Additionally, the model field was never included in SDAPI requests, which is required by llama-swap to route requests to the correct backend. Changes: - Server: Append to URL pathname instead of overwriting (same pattern as #5178) - Server: Pass model field through to sd-server payload - Client: Add model name text input for sd.cpp source settings - Client: Send model name in generate request payload * fix: fetch models from server and populate standard Model dropdown Instead of a separate text input for the model name, fetch the model list from the sd.cpp server's /v1/models endpoint and populate the standard Model dropdown. This provides a seamless experience where users just pick a model from the dropdown like any other source. Works with both standalone sd-server and proxy servers like llama-swap that expose multiple models via the OpenAI-compatible models endpoint. * fix: don't send clip_skip=1 to sd.cpp, it produces blank images sd-server generates blank white images when clip_skip is set to 1. Since clip_skip=1 means 'use all CLIP layers' (the default behavior), only send the parameter when it's > 1. * Fix eslint * Replace string appends with urlJoin * fix: convert URL strings to URL objects in sdcpp routes --------- Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>

fa9a28c6f3435d43619f60f0df8e1581de62d072

Alex Dills <alex@stechstudio.com>

Signed
2 files changed, +53 -6Ignore whitespace
public/scripts/extensions/stable-diffusion/index.js+30 -1
@@ -1815,6 +1815,34 @@ async function loadAutoSamplers() {
18151815 }
18161816}
18171817
1818+async function loadSdcppModels() {
1819+ if (!extension_settings.sd.sdcpp_url) {
1820+ return [{ value: '', text: 'N/A' }];
1821+ }
1822+
1823+ try {
1824+ const result = await fetch('/api/sd/sdcpp/models', {
1825+ method: 'POST',
1826+ headers: getRequestHeaders(),
1827+ body: JSON.stringify({ url: extension_settings.sd.sdcpp_url }),
1828+ });
1829+
1830+ if (!result.ok) {
1831+ return [{ value: '', text: 'N/A' }];
1832+ }
1833+
1834+ const data = await result.json();
1835+
1836+ if (data?.data?.length > 0) {
1837+ return data.data.map(model => ({ value: model.id, text: model.name || model.id }));
1838+ }
1839+ } catch (error) {
1840+ console.error('Failed to load sd.cpp models:', error);
1841+ }
1842+
1843+ return [{ value: '', text: 'N/A' }];
1844+}
1845+
18181846async function loadSdcppSamplers() {
18191847 // The sdcpp server does not provide an API for samplers, so we return the known list.
18201848 return ['euler', 'euler_a', 'heun', 'dpm2', 'dpm++2s_a', 'dpm++2m', 'dpm++2mv2', 'ipndm', 'ipndm_v', 'lcm', 'ddim_trailing', 'tcd'];
@@ -1910,7 +1938,7 @@ async function loadModels() {
19101938 models = await loadAutoModels();
19111939 break;
19121940 case sources.sdcpp:
1913- models = [{ value: '', text: 'N/A' }];
1941+ models = await loadSdcppModels();
19141942 break;
19151943 case sources.drawthings:
19161944 models = await loadDrawthingsModels();
@@ -3858,6 +3886,7 @@ async function generateAutoImage(prompt, negativePrompt, signal) {
38583886async function generateSdcppImage(prompt, negativePrompt, signal) {
38593887 const payload = {
38603888 url: extension_settings.sd.sdcpp_url,
3889+ model: extension_settings.sd.model || undefined,
38613890 prompt: prompt,
38623891 negative_prompt: negativePrompt,
38633892 steps: extension_settings.sd.steps,
src/endpoints/stable-diffusion.js+23 -5
@@ -833,8 +833,7 @@ const sdcpp = express.Router();
833833
834834sdcpp.post('/ping', async (request, response) => {
835835 try {
836836 const url = new URL(urlJoin(request.body.url, '/v1/images/generations'));
837- url.pathname = '/v1/images/generations';
838837
839838 const result = await fetch(url, { method: 'OPTIONS' });
840839 if (!result.ok) {
@@ -848,12 +847,29 @@ sdcpp.post('/ping', async (request, response) => {
848847 }
849848});
850849
850+sdcpp.post('/models', async (request, response) => {
851+ try {
852+ const url = new URL(urlJoin(request.body.url, '/v1/models'));
853+
854+ const result = await fetch(url);
855+ if (!result.ok) {
856+ throw new Error('stable-diffusion.cpp server returned an error.');
857+ }
858+
859+ const data = await result.json();
860+ return response.send(data);
861+ } catch (error) {
862+ console.error(error);
863+ return response.sendStatus(500);
864+ }
865+});
866+
851867sdcpp.post('/generate', async (request, response) => {
852868 try {
853869 const url = new URL(urlJoin(request.body.url, '/sdapi/v1/txt2img'));
854- url.pathname = '/sdapi/v1/txt2img';
855870
856871 const payload = {
872+ model: request.body.model,
857873 prompt: request.body.prompt,
858874 negative_prompt: request.body.negative_prompt,
859875 width: request.body.width,
@@ -864,7 +880,9 @@ sdcpp.post('/generate', async (request, response) => {
864880 batch_size: request.body.batch_size,
865881 sampler_name: request.body.sampler_name,
866882 scheduler: request.body.scheduler,
867- clip_skip: request.body.clip_skip,
883+ // sd.cpp produces blank images when clip_skip is 1, which is the
884+ // default (no skipping). Only send clip_skip when it's > 1.
885+ clip_skip: request.body.clip_skip > 1 ? request.body.clip_skip : undefined,
868886 };
869887
870888 for (const [key, value] of Object.entries(payload)) {