/** * @node sdcpp-architecture * @name SD.cpp Architecture Preset * @category sdcpp * @version 1.0.0 * @description Read the generation defaults SD.cpp uses for a model's architecture * @icon sliders-horizontal */ const credentialTypes = [ { id: 'sdcpp', label: 'SD.cpp Server', baseType: 'basic', description: 'The username and password you sign in to sdcpp-restapi with. The node exchanges them for a token before every call', usernameLabel: 'Username', passwordLabel: 'Password' } ]; const configSchema = { type: 'object', uiGroups: [ { title: 'Server', fields: ['serverUrl', 'credentialId'] }, { title: 'Model', fields: ['modelName', 'timeout'] } ], properties: { serverUrl: { type: 'string', title: 'Server URL', description: 'Base address of the sdcpp-restapi server', default: 'http://localhost:8077' }, credentialId: { type: 'string', title: 'Credential', description: 'A basic credential holding the sdcpp-restapi username and password', dynamicOptions: { source: 'credentials', filter: { type: ['sdcpp', 'basic'] } } }, modelName: { type: 'string', title: 'Model', description: 'Which model to look up. Leave empty to use whatever the server currently has loaded', dynamicOptions: { source: 'node', node: 'sdcpp-model', config: { listOnly: true }, itemsPath: 'models', valueKey: 'name', labelKey: 'name', needs: ['serverUrl', 'credentialId'] } }, timeout: { type: 'number', title: 'Timeout (ms)', description: 'How long to wait for the server to answer', default: 30000 } }, required: ['credentialId'] }; const inputSchema = { type: 'object', properties: { data: { type: 'any' } } }; const outputSchema = { type: 'object', properties: { found: { type: 'boolean', description: 'False when the server publishes no preset for this model' }, architecture: { type: 'string', description: 'Architecture id, such as Z-Image or Flux Schnell' }, architectureName: { type: 'string', description: 'Its display name' }, modelName: { type: 'string', description: 'The model this describes' }, description: { type: 'string', description: 'What the server says about this architecture' }, imageEditMode: { type: 'string', description: 'How this architecture takes an image to edit, when it can' }, requiredComponents: { type: 'object', description: 'VAE, LLM and so on that this architecture needs loaded' }, defaults: { type: 'object', description: 'The generation defaults, named the way the generation nodes name their settings' }, preset: { type: 'object', description: 'The preset exactly as the server describes it' } } }; function normalizeServer(url) { const value = String(url || '').trim(); if (!value) { throw new Error('SD.cpp: a server URL is required, such as http://localhost:8077'); } return value.replace(/\/+$/, ''); } function readCredential(credentialId) { const auth = smartbotic.credentials.get(credentialId); if (!auth || auth.success !== true) { throw new Error('SD.cpp: could not read the credential: ' + ((auth && auth.error) || 'unknown error')); } const value = auth.headerValue || ''; if (value.indexOf('Basic ') !== 0) { throw new Error('SD.cpp: the credential must be a basic one, holding the sdcpp-restapi ' + 'username and password'); } const decoded = smartbotic.utils.base64Decode(value.substring(6)); const separator = decoded.indexOf(':'); if (separator < 1) { throw new Error('SD.cpp: the credential is malformed, expected a username and a password'); } return { username: decoded.substring(0, separator), password: decoded.substring(separator + 1) }; } function call(options) { const response = smartbotic.http.request(options); let body = response.data; if (typeof body === 'string' && body.length > 0) { try { body = JSON.parse(body); } catch (e) { const snippet = body.substring(0, 200).replace(/\s+/g, ' '); throw new Error('SD.cpp: ' + options.what + ' returned HTTP ' + response.status + ' with a body that is not JSON: ' + snippet); } } if (response.status < 200 || response.status >= 300) { const detail = (body && (body.message || body.error)) || ('HTTP ' + response.status); throw new Error('SD.cpp: ' + options.what + ' failed: ' + detail); } return body || {}; } function login(server, credential, timeout) { const session = call({ method: 'POST', url: server + '/auth/login', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: credential.username, password: credential.password }), timeout: timeout, what: 'signing in' }); if (!session.token) { throw new Error('SD.cpp: the server accepted the login but returned no token'); } return session.token; } // The server names preset fields the way the generation endpoints do - // cfg_scale, cache_mode - while the nodes name their settings the way the rest // of the editor does. One rule converts between them, so a preset that gains a // field arrives without this node being touched. function toSetting(name) { const parts = String(name).split('_'); let out = parts[0]; for (let i = 1; i < parts.length; i++) { out += parts[i].charAt(0).toUpperCase() + parts[i].slice(1); } return out; } // Presets are keyed by their id, but a model can be known by a name or an // alias - "ZImage" and "Z Image" both mean Z-Image - so all three are tried. function findPreset(architectures, wanted) { const target = String(wanted || '').trim().toLowerCase(); if (!target) { return null; } const keys = Object.keys(architectures || {}); for (let i = 0; i < keys.length; i++) { const preset = architectures[keys[i]] || {}; const names = [keys[i], preset.id, preset.name]; const aliases = preset.aliases; if (aliases && typeof aliases.length === 'number') { for (let j = 0; j < aliases.length; j++) { names.push(aliases[j]); } } for (let j = 0; j < names.length; j++) { if (String(names[j] || '').trim().toLowerCase() === target) { return preset; } } } return null; } async function execute(config, input, context) { const server = normalizeServer(config.serverUrl); const timeout = config.timeout || 30000; const credential = readCredential(config.credentialId); const token = login(server, credential, timeout); const authHeader = { 'Authorization': 'Bearer ' + token }; let modelName = String(config.modelName || '').trim(); let architecture = ''; if (!modelName) { // /health needs no token, and for a model that is already loaded it // names the architecture outright - which detection does not always // manage from a filename alone. const health = call({ method: 'GET', url: server + '/health', timeout: timeout, what: 'reading server health' }); modelName = String(health.model_name || health.loading_model_name || '').trim(); architecture = String(health.model_architecture || ''); } if (!modelName && !architecture) { throw new Error('SD.cpp: no model given and the server has none loaded, so there is no ' + 'architecture to look up'); } if (!architecture && modelName) { const detection = call({ method: 'GET', url: server + '/architectures/detect?model=' + encodeURIComponent(modelName), headers: authHeader, timeout: timeout, what: 'detecting the architecture of "' + modelName + '"' }); const detected = detection.architecture || {}; architecture = String(detected.id || detected.name || ''); } const listing = call({ method: 'GET', url: server + '/architectures', headers: authHeader, timeout: timeout, what: 'reading the architecture presets' }); const preset = findPreset(listing.architectures, architecture) || {}; // The preset's generation defaults, under the names the generation nodes use // for them. This is what the Fill-in button on those nodes writes into the // form. const defaults = {}; const raw = preset.generationDefaults || {}; const keys = Object.keys(raw); for (let i = 0; i < keys.length; i++) { defaults[toSetting(keys[i])] = raw[keys[i]]; } if (!preset.id) { smartbotic.log.warn('SD.cpp: no preset for "' + (architecture || modelName) + '" - nothing to fill in from'); } else { smartbotic.log.info('SD.cpp: ' + modelName + ' is ' + preset.id + ', ' + keys.length + ' generation default(s)'); } return { found: !!preset.id, architecture: String(preset.id || architecture || ''), architectureName: String(preset.name || ''), modelName: modelName, description: String(preset.description || ''), imageEditMode: String(preset.imageEditMode || ''), requiredComponents: preset.requiredComponents || {}, defaults: defaults, preset: preset }; } module.exports = { credentialTypes, configSchema, inputSchema, outputSchema, execute };