55 estimateTokens ,
66 groupSessionsForMenu ,
77 messageDisplayText ,
8+ isModelRuntimeSelectable ,
9+ normalizeHermesModels ,
810 normalizeHermesSessions ,
911 normalizeHermesSkills ,
1012 normalizeToolActivity ,
@@ -21,7 +23,19 @@ import {
2123 normalizeColorMode ,
2224 resolveColorMode ,
2325} from './lib/appearance-themes.mjs' ;
24- import { discoverModelsFromRegistry } from './lib/model-discovery.mjs' ;
26+ import {
27+ MODEL_CATALOG_CACHE_STORAGE_KEY ,
28+ dashboardModelDiscoveryBaseUrl ,
29+ discoverModelsFromDashboard ,
30+ discoverModelsFromRegistry ,
31+ discoverModelsFromSessions ,
32+ mergeModelsWithRegistry ,
33+ modelCatalogCacheKey ,
34+ modelCatalogRefreshDecision ,
35+ normalizeCachedModelCatalog ,
36+ selectModelCatalogFallback ,
37+ shouldTrySessionModelFallback ,
38+ } from './lib/model-discovery.mjs' ;
2539import { extractMediaTags , resolveImageSource , resolvedGeneratedImageSources , stripGeneratedImageEchoes } from './lib/image-render.mjs' ;
2640import { modelLockRequestOutcome , readHermesSse , runSteerFailureState } from './lib/fulltab-runtime.mjs' ;
2741import { createDiffusionCanvas } from './lib/diffusion-canvas.mjs' ;
@@ -867,6 +881,9 @@ function renderModelPicker(query = '') {
867881 const button = document . createElement ( 'button' ) ;
868882 button . type = 'button' ;
869883 button . className = `model-choice ${ model . id === current . id ? 'selected' : '' } ` . trim ( ) ;
884+ const selectable = isModelRuntimeSelectable ( model ) ;
885+ button . disabled = ! selectable ;
886+ if ( ! selectable ) button . title = 'Observed from session history; not advertised as a requestable Hermes model.' ;
870887 const copy = document . createElement ( 'span' ) ;
871888 const provider = document . createElement ( 'small' ) ;
872889 const label = document . createElement ( 'strong' ) ;
@@ -876,18 +893,114 @@ function renderModelPicker(query = '') {
876893 selected . textContent = model . id === current . id ? '✓' : '' ;
877894 copy . append ( provider , label ) ;
878895 button . append ( copy , selected ) ;
879- button . addEventListener ( 'click' , ( ) => selectModel ( model ) ) ;
896+ if ( selectable ) button . addEventListener ( 'click' , ( ) => selectModel ( model ) ) ;
880897 els . modelList . append ( button ) ;
881898 }
882899 }
883900 if ( ! els . modelList . childElementCount ) els . modelList . textContent = 'No models found.' ;
884901 renderModelRuntimeOptions ( ) ;
885902}
886903
904+ async function readCachedModelCatalog ( ) {
905+ try {
906+ const stored = await chrome . storage . local . get ( [ MODEL_CATALOG_CACHE_STORAGE_KEY ] ) ;
907+ const key = modelCatalogCacheKey ( {
908+ gatewayMode : settings . gatewayMode ,
909+ gatewayUrl : settings . gatewayUrl ,
910+ profile : settings . activeProfile ,
911+ } ) ;
912+ return normalizeCachedModelCatalog ( stored ?. [ MODEL_CATALOG_CACHE_STORAGE_KEY ] ?. [ key ] ?. models ) ;
913+ } catch {
914+ return [ ] ;
915+ }
916+ }
917+
918+ async function writeCachedModelCatalog ( models = [ ] ) {
919+ const canonicalModels = normalizeCachedModelCatalog ( models ) ;
920+ if ( ! canonicalModels . length ) return ;
921+ try {
922+ const stored = await chrome . storage . local . get ( [ MODEL_CATALOG_CACHE_STORAGE_KEY ] ) ;
923+ const cache = stored ?. [ MODEL_CATALOG_CACHE_STORAGE_KEY ] && typeof stored [ MODEL_CATALOG_CACHE_STORAGE_KEY ] === 'object'
924+ ? stored [ MODEL_CATALOG_CACHE_STORAGE_KEY ]
925+ : { } ;
926+ const key = modelCatalogCacheKey ( {
927+ gatewayMode : settings . gatewayMode ,
928+ gatewayUrl : settings . gatewayUrl ,
929+ profile : settings . activeProfile ,
930+ } ) ;
931+ cache [ key ] = { savedAt : Date . now ( ) , models : canonicalModels } ;
932+ await chrome . storage . local . set ( { [ MODEL_CATALOG_CACHE_STORAGE_KEY ] : cache } ) ;
933+ } catch {
934+ // Catalog caching is resilience-only; storage failures must not block sync.
935+ }
936+ }
937+
887938async function loadModels ( { refresh = false } = { } ) {
888- const result = await discoverModelsFromRegistry ( { apiFetch : client . fetch , readJsonResponse : client . readJson , refresh } ) ;
889- if ( ! result . ok ) throw new Error ( result . error || 'Model discovery failed.' ) ;
890- availableModels = result . models ;
939+ const previousSelectedModel = settings . model ;
940+ let registryModels = [ ] ;
941+ let registrySource = '' ;
942+
943+ const registryResult = await discoverModelsFromRegistry ( { apiFetch : client . fetch , readJsonResponse : client . readJson , refresh } ) ;
944+ if ( registryResult . ok && registryResult . models . length ) {
945+ registryModels = normalizeHermesModels ( registryResult . models , settings . model ) ;
946+ registrySource = 'registry' ;
947+ } else {
948+ const dashboardResult = await discoverModelsFromDashboard ( {
949+ baseUrl : dashboardModelDiscoveryBaseUrl ( {
950+ gatewayMode : settings . gatewayMode ,
951+ gatewayUrl : settings . gatewayUrl ,
952+ } ) ,
953+ refresh,
954+ profile : settings . activeProfile ,
955+ } ) ;
956+ if ( dashboardResult . ok && dashboardResult . models . length ) {
957+ registryModels = normalizeHermesModels ( dashboardResult . models , settings . model ) ;
958+ registrySource = 'dashboard' ;
959+ } else {
960+ const cachedCatalogModels = await readCachedModelCatalog ( ) ;
961+ const cachedFallback = selectModelCatalogFallback ( { cachedModels : cachedCatalogModels } ) ;
962+ if ( cachedFallback . models . length ) {
963+ registryModels = normalizeHermesModels ( cachedFallback . models , settings . model ) ;
964+ registrySource = cachedFallback . source ;
965+ } else {
966+ const response = await client . fetch ( '/v1/models' , { method : 'GET' } ) ;
967+ const payload = await client . readJson ( response ) ;
968+ if ( ! response . ok ) throw new Error ( payload ?. error ?. message || payload ?. error || `Model list failed (${ response . status } )` ) ;
969+ registryModels = normalizeHermesModels ( payload , settings . model ) ;
970+ registrySource = 'v1' ;
971+ }
972+ }
973+ }
974+
975+ if ( registryModels . length && [ 'registry' , 'dashboard' ] . includes ( registrySource ) ) {
976+ await writeCachedModelCatalog ( registryModels ) ;
977+ }
978+
979+ if ( shouldTrySessionModelFallback ( {
980+ registryModels,
981+ registrySource,
982+ defaultModelId : 'hermes-agent' ,
983+ } ) ) {
984+ const sessionResult = await discoverModelsFromSessions ( { apiFetch : client . fetch , readJsonResponse : client . readJson } ) ;
985+ if ( sessionResult . ok && sessionResult . models . length ) {
986+ registryModels = normalizeHermesModels (
987+ mergeModelsWithRegistry ( { registryModels, sessionModels : sessionResult . models } ) ,
988+ settings . model ,
989+ ) ;
990+ registrySource = 'sessions' ;
991+ }
992+ }
993+
994+ const refreshDecision = modelCatalogRefreshDecision ( {
995+ previousSelectedModel,
996+ discoveredModels : registryModels ,
997+ refresh,
998+ } ) ;
999+ if ( refreshDecision . keepPreviousSelection ) {
1000+ registryModels = normalizeHermesModels ( registryModels , refreshDecision . selectedModel ) ;
1001+ }
1002+
1003+ availableModels = registryModels ;
8911004 const current = effectiveModel ( ) ;
8921005 els . modelLabel . textContent = current . label ;
8931006 renderModelPicker ( els . modelSearch . value ) ;
0 commit comments