@@ -2368,6 +2368,14 @@ <h3 style="margin:0">☁️ Add cloud model</h3>
23682368 < label style ="font-size:12px;font-weight:500;display:block;margin-bottom:4px "> Provider</ label >
23692369 < select id ="ac-provider " class ="tool-input " style ="width:100%;margin-bottom:12px " onchange ="acProviderChanged() "> </ select >
23702370
2371+ < div id ="ac-custom-fields " style ="display:none ">
2372+ < label style ="font-size:12px;font-weight:500;display:block;margin-bottom:4px "> Nome (a tua scelta)</ label >
2373+ < input id ="ac-name " class ="tool-input " placeholder ="es. Server di casa / Vast.ai " style ="width:100%;margin-bottom:12px ">
2374+ < label style ="font-size:12px;font-weight:500;display:block;margin-bottom:4px "> Indirizzo server (OpenAI/Ollama compatibile)</ label >
2375+ < input id ="ac-baseurl " class ="tool-input " placeholder ="es. http://192.168.1.50:11434 oppure https://host-vast:porta/v1 " style ="width:100%;margin-bottom:4px ">
2376+ < div style ="font-size:11px;color:var(--text3);margin-bottom:12px "> Accetta IP:porta, .../v1 o l'URL completo. Per Ollama remoto basta http://IP:11434</ div >
2377+ </ div >
2378+
23712379 < label style ="font-size:12px;font-weight:500;display:block;margin-bottom:4px "> Tipo</ label >
23722380 < select id ="ac-type " class ="tool-input " style ="width:100%;margin-bottom:12px ">
23732381 < option value ="llm "> LLM (chat / testo)</ option >
@@ -3006,6 +3014,8 @@ <h3 style="margin:0">☁️ Add cloud model</h3>
30063014let selectedModelIsCloud = false ; // true when the picked assistant is a cloud model
30073015let selectedCloudProvider = '' ; // provider id when cloud (e.g. "openai")
30083016let selectedCloudModel = '' ; // model id when cloud (e.g. "gpt-4o")
3017+ let selectedCustomBaseURL = '' ; // base URL when using a custom/self-hosted endpoint
3018+ let selectedCustomKey = '' ; // optional key for the custom endpoint
30093019let chatHistory = [ ] ;
30103020let sessions = [ ] ;
30113021let currentSession = null ;
@@ -3279,6 +3289,10 @@ <h3 style="margin:0">☁️ Add cloud model</h3>
32793289 const v = `cloud:${ cm . provider } :${ cm . id } ` ;
32803290 if ( p && p . has_key && ! seen [ v ] ) { seen [ v ] = 1 ; models . push ( { v, l : cm . label || cm . id , tag : '☁️ ' + ( p . name || cm . provider ) , cloud : true } ) ; }
32813291 }
3292+ // Custom / self-hosted endpoints (always usable).
3293+ for ( const ep of getCustomEndpoints ( ) ) {
3294+ models . push ( { v : 'customep:' + ep . id , l : ep . label || ep . model , tag : '🖥 ' + ( ep . name || 'server' ) , cloud : true } ) ;
3295+ }
32823296 }
32833297 buildModelDD ( models ) ;
32843298 if ( ! models . find ( m => m . v === selectedModel ) ) {
@@ -3388,6 +3402,15 @@ <h3 style="margin:0">☁️ Add cloud model</h3>
33883402function getCustomCloudModels ( ) {
33893403 try { return JSON . parse ( localStorage . getItem ( 'vortelio_cloud_models' ) || '[]' ) ; } catch { return [ ] ; }
33903404}
3405+ // Custom/self-hosted endpoints (vast.ai, home server, remote Ollama): {id,name,baseURL,key,model,label}
3406+ function getCustomEndpoints ( ) {
3407+ try { return JSON . parse ( localStorage . getItem ( 'vortelio_custom_endpoints' ) || '[]' ) ; } catch { return [ ] ; }
3408+ }
3409+ function saveCustomEndpoint ( ep ) {
3410+ const list = getCustomEndpoints ( ) . filter ( e => e . id !== ep . id ) ;
3411+ list . push ( ep ) ;
3412+ localStorage . setItem ( 'vortelio_custom_endpoints' , JSON . stringify ( list ) ) ;
3413+ }
33913414function saveCustomCloudModel ( provider , id , label ) {
33923415 const list = getCustomCloudModels ( ) ;
33933416 if ( ! list . some ( m => m . provider === provider && m . id === id ) ) {
@@ -3398,17 +3421,31 @@ <h3 style="margin:0">☁️ Add cloud model</h3>
33983421function openAddCloudModel ( ) {
33993422 const sel = document . getElementById ( 'ac-provider' ) ;
34003423 if ( ! _cloudProviders || ! _cloudProviders . length ) { loadCloudProviders ( ) . then ( openAddCloudModel ) ; return ; }
3401- sel . innerHTML = _cloudProviders . map ( p => `<option value="${ esc ( p . id ) } ">${ esc ( p . name ) } ${ p . has_key ? ' ✓' : '' } </option>` ) . join ( '' ) ;
3424+ sel . innerHTML = _cloudProviders . map ( p => `<option value="${ esc ( p . id ) } ">${ esc ( p . name ) } ${ p . has_key ? ' ✓' : '' } </option>` ) . join ( '' )
3425+ + `<option value="__custom__">🖥 Server personalizzato (vast.ai / casa / Ollama)</option>` ;
34023426 acProviderChanged ( ) ;
34033427 document . getElementById ( 'ac-key' ) . value = '' ;
34043428 document . getElementById ( 'addcloud-overlay' ) . classList . add ( 'show' ) ;
34053429}
34063430function closeAddCloudModel ( ) { document . getElementById ( 'addcloud-overlay' ) . classList . remove ( 'show' ) ; }
34073431function acProviderChanged ( ) {
3408- const p = ( _cloudProviders || [ ] ) . find ( x => x . id === document . getElementById ( 'ac-provider' ) . value ) ;
3432+ const pid = document . getElementById ( 'ac-provider' ) . value ;
3433+ const isCustom = pid === '__custom__' ;
3434+ document . getElementById ( 'ac-custom-fields' ) . style . display = isCustom ? 'block' : 'none' ;
3435+ if ( isCustom ) {
3436+ // Self-hosted/remote endpoint: model is a free-typed id, key optional.
3437+ document . getElementById ( 'ac-model' ) . innerHTML = `<option value="__custom__">Scrivi l'ID del modello</option>` ;
3438+ acModelChanged ( ) ;
3439+ document . getElementById ( 'ac-model-custom' ) . placeholder = 'ID modello sul server (es. llama3.1:8b, qwen2.5:7b)' ;
3440+ document . getElementById ( 'ac-key-note' ) . textContent = '— opzionale (solo se il server la richiede)' ;
3441+ document . getElementById ( 'ac-key-link' ) . style . display = 'none' ;
3442+ return ;
3443+ }
3444+ const p = ( _cloudProviders || [ ] ) . find ( x => x . id === pid ) ;
34093445 let opts = ( ( p && p . models ) || [ ] ) . map ( cm => `<option value="${ esc ( cm . id ) } ">${ esc ( cm . label || cm . id ) } </option>` ) . join ( '' ) ;
34103446 opts += `<option value="__custom__">✏️ Altro… (scrivi l'ID)</option>` ;
34113447 document . getElementById ( 'ac-model' ) . innerHTML = opts ;
3448+ document . getElementById ( 'ac-model-custom' ) . placeholder = 'ID modello (es. gpt-4.1, claude-opus-4)' ;
34123449 acModelChanged ( ) ;
34133450 document . getElementById ( 'ac-key-note' ) . textContent = ( p && p . has_key ) ? '— già salvata (lascia vuoto per tenerla)' : '— richiesta' ;
34143451 const link = document . getElementById ( 'ac-key-link' ) ;
@@ -3420,9 +3457,26 @@ <h3 style="margin:0">☁️ Add cloud model</h3>
34203457}
34213458async function saveAddCloudModel ( ) {
34223459 const pid = document . getElementById ( 'ac-provider' ) . value ;
3423- const p = ( _cloudProviders || [ ] ) . find ( x => x . id === pid ) ;
34243460 let modelId = document . getElementById ( 'ac-model' ) . value ;
34253461 if ( modelId === '__custom__' ) modelId = document . getElementById ( 'ac-model-custom' ) . value . trim ( ) ;
3462+
3463+ // Custom / self-hosted server (vast.ai, home server, remote Ollama).
3464+ if ( pid === '__custom__' ) {
3465+ const name = ( document . getElementById ( 'ac-name' ) . value . trim ( ) ) || 'Server' ;
3466+ const baseURL = document . getElementById ( 'ac-baseurl' ) . value . trim ( ) ;
3467+ const key = document . getElementById ( 'ac-key' ) . value . trim ( ) ;
3468+ if ( ! baseURL ) { toast ( 'Inserisci l\'indirizzo del server' , 'err' ) ; return ; }
3469+ if ( ! modelId ) { toast ( 'Scrivi l\'ID del modello (es. llama3.1:8b)' , 'err' ) ; return ; }
3470+ const id = 'ep' + Date . now ( ) ;
3471+ saveCustomEndpoint ( { id, name, baseURL, key, model : modelId , label : modelId + ' · ' + name } ) ;
3472+ updateModelDDForDomain ( ) ;
3473+ selectModel ( 'customep:' + id , modelId + ' · ' + name ) ;
3474+ closeAddCloudModel ( ) ;
3475+ toast ( '🖥 Server aggiunto: ' + name , 'ok' ) ;
3476+ return ;
3477+ }
3478+
3479+ const p = ( _cloudProviders || [ ] ) . find ( x => x . id === pid ) ;
34263480 if ( ! modelId ) { toast ( 'Scegli o scrivi un modello' , 'err' ) ; return ; }
34273481 const key = document . getElementById ( 'ac-key' ) . value . trim ( ) ;
34283482 if ( ( ! p || ! p . has_key ) && ! key ) { toast ( 'Inserisci la API key per questo provider' , 'err' ) ; return ; }
@@ -3452,7 +3506,13 @@ <h3 style="margin:0">☁️ Add cloud model</h3>
34523506
34533507function selectModel ( v , l ) {
34543508 selectedModel = v ;
3455- if ( v . startsWith ( 'cloud:' ) ) {
3509+ selectedCustomBaseURL = '' ; selectedCustomKey = '' ;
3510+ if ( v . startsWith ( 'customep:' ) ) {
3511+ // A saved custom/self-hosted endpoint (vast.ai, home server, remote Ollama).
3512+ const ep = getCustomEndpoints ( ) . find ( e => e . id === v . slice ( 9 ) ) ;
3513+ selectedModelIsCloud = true ; selectedCloudProvider = 'custom' ;
3514+ if ( ep ) { selectedCloudModel = ep . model ; selectedCustomBaseURL = ep . baseURL ; selectedCustomKey = ep . key || '' ; l = l || ep . label ; }
3515+ } else if ( v . startsWith ( 'cloud:' ) ) {
34563516 const parts = v . split ( ':' ) ;
34573517 selectedModelIsCloud = true ;
34583518 selectedCloudProvider = parts [ 1 ] || '' ;
@@ -4532,6 +4592,8 @@ <h3 style="margin:0">☁️ Add cloud model</h3>
45324592// Shared by the unified assistant chat and the legacy Cloud Models panel.
45334593async function streamCloudInto ( provider , model , history , content , canvas , signal ) {
45344594 const payload = { provider, model, messages : history } ;
4595+ // Custom / self-hosted endpoint (vast.ai, home server, remote Ollama).
4596+ if ( provider === 'custom' ) { payload . base_url = selectedCustomBaseURL || '' ; payload . key = selectedCustomKey || '' ; }
45354597 const ag = buildAgenticPayload ( ) ; if ( ag ) payload . agentic = ag ;
45364598 const r = await apiFetch ( '/api/cloud/chat' , {
45374599 method : 'POST' , headers : { 'Content-Type' :'application/json' } ,
0 commit comments