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>
Signed| @@ -1815,6 +1815,34 @@ async function loadAutoSamplers() { | ||
| 1815 | 1815 | } |
| 1816 | 1816 | } |
| 1817 | 1817 | |
| 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 | + | |
| 1818 | 1846 | async function loadSdcppSamplers() { |
| 1819 | 1847 | // The sdcpp server does not provide an API for samplers, so we return the known list. |
| 1820 | 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 | 1938 | models = await loadAutoModels(); |
| 1911 | 1939 | break; |
| 1912 | 1940 | case sources.sdcpp: |
| 1913 | - models = [{ value: '', text: 'N/A' }]; | |
| 1941 | + models = await loadSdcppModels(); | |
| 1914 | 1942 | break; |
| 1915 | 1943 | case sources.drawthings: |
| 1916 | 1944 | models = await loadDrawthingsModels(); |
| @@ -3858,6 +3886,7 @@ async function generateAutoImage(prompt, negativePrompt, signal) { | ||
| 3858 | 3886 | async function generateSdcppImage(prompt, negativePrompt, signal) { |
| 3859 | 3887 | const payload = { |
| 3860 | 3888 | url: extension_settings.sd.sdcpp_url, |
| 3889 | + model: extension_settings.sd.model || undefined, | |
| 3861 | 3890 | prompt: prompt, |
| 3862 | 3891 | negative_prompt: negativePrompt, |
| 3863 | 3892 | steps: extension_settings.sd.steps, |
| @@ -833,8 +833,7 @@ const sdcpp = express.Router(); | ||
| 833 | 833 | |
| 834 | 834 | sdcpp.post('/ping', async (request, response) => { |
| 835 | 835 | try { |
| 836 | 836 | const url = new URL(urlJoin(request.body.url, '/v1/images/generations')); |
| 837 | - url.pathname = '/v1/images/generations'; | |
| 838 | 837 | |
| 839 | 838 | const result = await fetch(url, { method: 'OPTIONS' }); |
| 840 | 839 | if (!result.ok) { |
| @@ -848,12 +847,29 @@ sdcpp.post('/ping', async (request, response) => { | ||
| 848 | 847 | } |
| 849 | 848 | }); |
| 850 | 849 | |
| 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 | + | |
| 851 | 867 | sdcpp.post('/generate', async (request, response) => { |
| 852 | 868 | try { |
| 853 | 869 | const url = new URL(urlJoin(request.body.url, '/sdapi/v1/txt2img')); |
| 854 | - url.pathname = '/sdapi/v1/txt2img'; | |
| 855 | 870 | |
| 856 | 871 | const payload = { |
| 872 | + model: request.body.model, | |
| 857 | 873 | prompt: request.body.prompt, |
| 858 | 874 | negative_prompt: request.body.negative_prompt, |
| 859 | 875 | width: request.body.width, |
| @@ -864,7 +880,9 @@ sdcpp.post('/generate', async (request, response) => { | ||
| 864 | 880 | batch_size: request.body.batch_size, |
| 865 | 881 | sampler_name: request.body.sampler_name, |
| 866 | 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 | }; |
| 869 | 887 | |
| 870 | 888 | for (const [key, value] of Object.entries(payload)) { |