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 -6Showing whitespace changes
public/scripts/extensions/stable-diffusion/index.js+30 -1
@@ -1815,6 +1815,34 @@ async function loadAutoSamplers() {
1815 }1815 }
1816}1816}
18171817
1818async 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
1818async function loadSdcppSamplers() {1846async function loadSdcppSamplers() {
1819 // The sdcpp server does not provide an API for samplers, so we return the known list.1847 // The sdcpp server does not provide an API for samplers, so we return the known list.
1820 return ['euler', 'euler_a', 'heun', 'dpm2', 'dpm++2s_a', 'dpm++2m', 'dpm++2mv2', 'ipndm', 'ipndm_v', 'lcm', 'ddim_trailing', 'tcd'];1848 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() {
1910 models = await loadAutoModels();1938 models = await loadAutoModels();
1911 break;1939 break;
1912 case sources.sdcpp:1940 case sources.sdcpp:
1913 models = [{ value: '', text: 'N/A' }];1941 models = await loadSdcppModels();
1914 break;1942 break;
1915 case sources.drawthings:1943 case sources.drawthings:
1916 models = await loadDrawthingsModels();1944 models = await loadDrawthingsModels();
@@ -3858,6 +3886,7 @@ async function generateAutoImage(prompt, negativePrompt, signal) {
3858async function generateSdcppImage(prompt, negativePrompt, signal) {3886async function generateSdcppImage(prompt, negativePrompt, signal) {
3859 const payload = {3887 const payload = {
3860 url: extension_settings.sd.sdcpp_url,3888 url: extension_settings.sd.sdcpp_url,
3889 model: extension_settings.sd.model || undefined,
3861 prompt: prompt,3890 prompt: prompt,
3862 negative_prompt: negativePrompt,3891 negative_prompt: negativePrompt,
3863 steps: extension_settings.sd.steps,3892 steps: extension_settings.sd.steps,
src/endpoints/stable-diffusion.js+23 -5
@@ -833,8 +833,7 @@ const sdcpp = express.Router();
833833
834sdcpp.post('/ping', async (request, response) => {834sdcpp.post('/ping', async (request, response) => {
835 try {835 try {
836 const url = new URL(request.body.url);836 const url = new URL(urlJoin(request.body.url, '/v1/images/generations'));
837 url.pathname = '/v1/images/generations';
838837
839 const result = await fetch(url, { method: 'OPTIONS' });838 const result = await fetch(url, { method: 'OPTIONS' });
840 if (!result.ok) {839 if (!result.ok) {
@@ -848,12 +847,29 @@ sdcpp.post('/ping', async (request, response) => {
848 }847 }
849});848});
850849
850sdcpp.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
851sdcpp.post('/generate', async (request, response) => {867sdcpp.post('/generate', async (request, response) => {
852 try {868 try {
853 const url = new URL(request.body.url);869 const url = new URL(urlJoin(request.body.url, '/sdapi/v1/txt2img'));
854 url.pathname = '/sdapi/v1/txt2img';
855870
856 const payload = {871 const payload = {
872 model: request.body.model,
857 prompt: request.body.prompt,873 prompt: request.body.prompt,
858 negative_prompt: request.body.negative_prompt,874 negative_prompt: request.body.negative_prompt,
859 width: request.body.width,875 width: request.body.width,
@@ -864,7 +880,9 @@ sdcpp.post('/generate', async (request, response) => {
864 batch_size: request.body.batch_size,880 batch_size: request.body.batch_size,
865 sampler_name: request.body.sampler_name,881 sampler_name: request.body.sampler_name,
866 scheduler: request.body.scheduler,882 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,
868 };886 };
869887
870 for (const [key, value] of Object.entries(payload)) {888 for (const [key, value] of Object.entries(payload)) {