From c784ef7a12499fd3aa5ff00bceca3bc346521ac4 Mon Sep 17 00:00:00 2001 From: webtecnica Date: Thu, 24 Sep 2026 08:50:30 -0300 Subject: [PATCH] fix(ui): preserve endpoint-style custom slugs host:port in model parsers (#6884) --- static/ui.js | 40 ++++++++++++++++--- ...onfigured_model_picker_provider_routing.py | 21 +++++++++- 2 files changed, 55 insertions(+), 6 deletions(-) diff --git a/static/ui.js b/static/ui.js index e45bf1eb..62997d58 100644 --- a/static/ui.js +++ b/static/ui.js @@ -2982,9 +2982,19 @@ function _getOptionProviderId(opt){ const value=String(opt.value||''); if(value.startsWith('@') && value.includes(':')){ // Non-greedy parse for @custom:: — provider is the slug only. - // For @custom:backup:model-a:free → provider="custom:backup", not "custom:backup:model-a" + // Preserves endpoint-style host:port custom slugs (e.g. custom:localhost:11434) + // while keeping colon-bearing model ids (e.g. @custom:backup:model-a:free -> custom:backup). if(value.startsWith('@custom:')){ const afterCustom=value.substring('@custom:'.length); + const parts=afterCustom.split(':'); + if(parts.length>=3 && /^\d+$/.test(parts[1])){ + const port=parseInt(parts[1], 10); + const host=parts[0]; + const hl=host.toLowerCase(); + if(port>=1 && port<=65535 && (hl==='localhost' || host.includes('.'))){ + return 'custom:'+host+':'+parts[1]; + } + } const firstColon=afterCustom.indexOf(':'); if(firstColon>=0) return 'custom:'+afterCustom.substring(0,firstColon); return 'custom:'+afterCustom; @@ -2998,9 +3008,19 @@ function _providerFromModelValue(modelId){ const value=String(modelId||'').trim(); if(value.startsWith('@')&&value.includes(':')){ // Non-greedy parse for @custom:: — provider is the slug only. - // For @custom:backup:model-a:free → provider="custom:backup", not "custom:backup:model-a" + // Preserves endpoint-style host:port custom slugs (e.g. custom:localhost:11434) + // while keeping colon-bearing model ids (e.g. @custom:backup:model-a:free -> custom:backup). if(value.startsWith('@custom:')){ const afterCustom=value.substring('@custom:'.length); + const parts=afterCustom.split(':'); + if(parts.length>=3 && /^\d+$/.test(parts[1])){ + const port=parseInt(parts[1], 10); + const host=parts[0]; + const hl=host.toLowerCase(); + if(port>=1 && port<=65535 && (hl==='localhost' || host.includes('.'))){ + return 'custom:'+host+':'+parts[1]; + } + } const firstColon=afterCustom.indexOf(':'); if(firstColon>=0) return 'custom:'+afterCustom.substring(0,firstColon); return 'custom:'+afterCustom; @@ -3018,9 +3038,19 @@ function _modelPickerOptionIdentity(modelId, providerId){ if(exactPrefix && value.toLowerCase().startsWith(exactPrefix.toLowerCase())){ value=value.substring(exactPrefix.length); }else if(value.startsWith('@custom:')){ - const namedProvider=value.substring('@custom:'.length); - const splitAt=namedProvider.indexOf(':'); - value=splitAt>=0 ? namedProvider.substring(splitAt+1) : namedProvider; + const afterCustom=value.substring('@custom:'.length); + const parts=afterCustom.split(':'); + let splitAt=-1; + if(parts.length>=3 && /^\d+$/.test(parts[1])){ + const port=parseInt(parts[1], 10); + const host=parts[0]; + const hl=host.toLowerCase(); + if(port>=1 && port<=65535 && (hl==='localhost' || host.includes('.'))){ + splitAt=parts[0].length + 1 + parts[1].length; + } + } + if(splitAt<0) splitAt=afterCustom.indexOf(':'); + value=splitAt>=0 ? afterCustom.substring(splitAt+1) : afterCustom; }else{ value=value.substring(value.indexOf(':')+1); } diff --git a/tests/test_configured_model_picker_provider_routing.py b/tests/test_configured_model_picker_provider_routing.py index ca4daf30..4a28b289 100644 --- a/tests/test_configured_model_picker_provider_routing.py +++ b/tests/test_configured_model_picker_provider_routing.py @@ -229,9 +229,16 @@ const modelAFree = { dataset: {}, // no data-model — normal catalog render path parentElement: group, }; +const localhostGroup = {tagName: 'OPTGROUP', dataset: {provider: 'custom:localhost:11434'}}; +const llama = { + value: '@custom:localhost:11434:llama3.2', + textContent: 'llama3.2', + dataset: {}, + parentElement: localhostGroup, +}; const select = { id: 'modelSelect', - options: [luna, sol, modelAFree], + options: [luna, sol, modelAFree, llama], querySelectorAll() { return []; }, get selectedOptions() { return [sol]; }, get value() { return sol.value; }, @@ -242,6 +249,8 @@ process.stdout.write(JSON.stringify({ nonDefault: _modelStateForSelect(select, '@custom:hetmer.net:sol'), defaultUnprefixed: _modelStateForSelect(select, 'luna'), colonBearingModel: _modelStateForSelect(select, '@custom:hetmer.net:model-a:free'), + localhostEndpoint: _modelStateForSelect(select, '@custom:localhost:11434:llama3.2'), + missingOptionCustomInput: _modelStateForSelect(select, '@custom:localhost:11434:mistral-custom'), })); """ @@ -276,6 +285,16 @@ def test_non_default_named_custom_provider_model_strips_qualified_prefix(): "model": "model-a:free", "model_provider": "custom:hetmer.net", } + # Endpoint-style custom provider (e.g. host:port derived from base_url authority): + # preserves full custom:localhost:11434 for both catalog and missing-option paths. + assert payload["localhostEndpoint"] == { + "model": "llama3.2", + "model_provider": "custom:localhost:11434", + } + assert payload["missingOptionCustomInput"] == { + "model": "mistral-custom", + "model_provider": "custom:localhost:11434", + } # Round-trip: send -> persist -> restore for a colon-bearing named