@@ -8,10 +8,37 @@ import { InstantiationType, registerSingleton } from '../../instantiation/common
88import { IVoidSettingsService } from './voidSettingsService.js' ;
99import { ILLMMessageService } from './llmMessageService.js' ;
1010import { Emitter , Event } from '../../../base/common/event.js' ;
11- import { Disposable } from '../../../base/common/lifecycle.js' ;
11+ import { Disposable , IDisposable } from '../../../base/common/lifecycle.js' ;
12+ import { ProviderName , SettingsOfProvider } from './voidSettingsTypes.js' ;
13+ import { OllamaModelResponse , OpenaiCompatibleModelResponse } from './llmMessageTypes.js' ;
1214
1315
14- export type RefreshModelState = 'done' | 'loading'
16+ export const refreshableProviderNames = [ 'ollama' , 'openAICompatible' ] satisfies ProviderName [ ]
17+
18+ export type RefreshableProviderName = typeof refreshableProviderNames [ number ]
19+
20+
21+ type RefreshableState = {
22+ state : 'init' ,
23+ timeoutId : null ,
24+ } | {
25+ state : 'refreshing' ,
26+ timeoutId : NodeJS . Timeout | null ,
27+ } | {
28+ state : 'success' ,
29+ timeoutId : null ,
30+ }
31+
32+
33+ export type RefreshModelStateOfProvider = Record < RefreshableProviderName , RefreshableState >
34+
35+
36+
37+ const refreshBasedOn : { [ k in RefreshableProviderName ] : ( keyof SettingsOfProvider [ k ] ) [ ] } = {
38+ ollama : [ 'enabled' , 'endpoint' ] ,
39+ openAICompatible : [ 'enabled' , 'endpoint' , 'apiKey' ] ,
40+ }
41+ const REFRESH_INTERVAL = 5000
1542
1643// element-wise equals
1744function eq < T > ( a : T [ ] , b : T [ ] ) : boolean {
@@ -23,9 +50,9 @@ function eq<T>(a: T[], b: T[]): boolean {
2350}
2451export interface IRefreshModelService {
2552 readonly _serviceBrand : undefined ;
26- refreshOllamaModels ( ) : void ;
27- onDidChangeState : Event < void > ;
28- state : RefreshModelState ;
53+ refreshModels : ( providerName : RefreshableProviderName ) => Promise < void > ;
54+ onDidChangeState : Event < RefreshableProviderName > ;
55+ state : RefreshModelStateOfProvider ;
2956}
3057
3158export const IRefreshModelService = createDecorator < IRefreshModelService > ( 'RefreshModelService' ) ;
@@ -34,71 +61,120 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
3461
3562 readonly _serviceBrand : undefined ;
3663
37- private readonly _onDidChangeState = new Emitter < void > ( ) ;
38- readonly onDidChangeState : Event < void > = this . _onDidChangeState . event ; // this is primarily for use in react, so react can listen + update on state changes
64+ private readonly _onDidChangeState = new Emitter < RefreshableProviderName > ( ) ;
65+ readonly onDidChangeState : Event < RefreshableProviderName > = this . _onDidChangeState . event ; // this is primarily for use in react, so react can listen + update on state changes
3966
4067 constructor (
4168 @IVoidSettingsService private readonly voidSettingsService : IVoidSettingsService ,
4269 @ILLMMessageService private readonly llmMessageService : ILLMMessageService ,
4370 ) {
4471 super ( )
4572
46- // on mount, refresh ollama models
47- this . refreshOllamaModels ( )
48-
49- // every time ollama.enabled changes, refresh ollama models, like useEffect
50- let relevantVals = ( ) => [ this . voidSettingsService . state . settingsOfProvider . ollama . enabled , this . voidSettingsService . state . settingsOfProvider . ollama . endpoint ]
51- let prevVals = relevantVals ( )
52- this . _register (
53- this . voidSettingsService . onDidChangeState ( ( ) => { // we might want to debounce this
54- const newVals = relevantVals ( )
55- if ( ! eq ( prevVals , newVals ) ) {
56- this . refreshOllamaModels ( )
57- prevVals = newVals
58- }
59- } )
60- )
6173
62- }
74+ const disposables : Set < IDisposable > = new Set ( )
75+
76+
77+ const startRefreshing = ( ) => {
78+ this . _clearAllTimeouts ( )
79+ disposables . forEach ( d => d . dispose ( ) )
80+ disposables . clear ( )
81+
82+ if ( ! voidSettingsService . state . featureFlagSettings . autoRefreshModels ) return
6383
64- state : RefreshModelState = 'done'
84+ for ( const providerName of refreshableProviderNames ) {
6585
66- private _timeoutId : NodeJS . Timeout | null = null
67- private _cancelTimeout = ( ) => {
68- if ( this . _timeoutId ) {
69- clearTimeout ( this . _timeoutId )
70- this . _timeoutId = null
86+ const refresh = ( ) => {
87+ // const { enabled } = this.voidSettingsService.state.settingsOfProvider[providerName]
88+ this . refreshModels ( providerName , { enableProviderOnSuccess : true } ) // enable the provider on success
89+ }
90+
91+ refresh ( )
92+
93+ // every time providerName.enabled changes, refresh models too, like a useEffect
94+ let relevantVals = ( ) => refreshBasedOn [ providerName ] . map ( settingName => this . voidSettingsService . state . settingsOfProvider [ providerName ] [ settingName ] )
95+ let prevVals = relevantVals ( ) // each iteration of a for loop has its own context and vars, so this is ok
96+ disposables . add (
97+ this . voidSettingsService . onDidChangeState ( ( ) => { // we might want to debounce this
98+ const newVals = relevantVals ( )
99+ if ( ! eq ( prevVals , newVals ) ) {
100+ refresh ( )
101+ prevVals = newVals
102+ }
103+ } )
104+ )
105+ }
71106 }
107+
108+ // on mount (when get init settings state), and if a relevant feature flag changes (detected natively right now by refreshing if any flag changes), start refreshing models
109+ voidSettingsService . waitForInitState . then ( ( ) => {
110+ startRefreshing ( )
111+ this . _register (
112+ voidSettingsService . onDidChangeState ( ( type ) => { if ( type === 'featureFlagSettings' ) startRefreshing ( ) } )
113+ )
114+ } )
115+
72116 }
73- async refreshOllamaModels ( ) {
74- // cancel any existing poll
75- this . _cancelTimeout ( )
76117
77- // if ollama is disabled, obivously done
78- if ( ! this . voidSettingsService . state . settingsOfProvider . ollama . enabled ) {
79- this . _setState ( 'done' )
80- return
81- }
118+ state : RefreshModelStateOfProvider = {
119+ ollama : { state : 'init' , timeoutId : null } ,
120+ openAICompatible : { state : 'init' , timeoutId : null } ,
121+ }
122+
123+
124+ // start listening for models (and don't stop until success)
125+ async refreshModels ( providerName : RefreshableProviderName , options ?: { enableProviderOnSuccess ?: boolean } ) {
126+ this . _clearProviderTimeout ( providerName )
82127
83128 // start loading models
84- this . _setState ( 'loading' )
129+ this . _setRefreshState ( providerName , 'refreshing' )
130+
131+ const fn = providerName === 'ollama' ? this . llmMessageService . ollamaList
132+ : providerName === 'openAICompatible' ? this . llmMessageService . openAICompatibleList
133+ : ( ) => { }
85134
86- this . llmMessageService . ollamaList ( {
135+ fn ( {
87136 onSuccess : ( { models } ) => {
88- this . voidSettingsService . setDefaultModels ( 'ollama' , models . map ( model => model . name ) )
89- this . _setState ( 'done' )
137+ this . voidSettingsService . setDefaultModels ( providerName , models . map ( model => {
138+ if ( providerName === 'ollama' ) return ( model as OllamaModelResponse ) . name
139+ else if ( providerName === 'openAICompatible' ) return ( model as OpenaiCompatibleModelResponse ) . id
140+ else throw new Error ( 'refreshMode fn: unknown provider' , providerName )
141+ } ) )
142+
143+ if ( options ?. enableProviderOnSuccess )
144+ this . voidSettingsService . setSettingOfProvider ( providerName , 'enabled' , true )
145+
146+ this . _setRefreshState ( providerName , 'success' )
90147 } ,
91148 onError : ( { error } ) => {
92149 // poll
93- console . log ( 'retrying ollamaList:' , error )
94- this . _timeoutId = setTimeout ( ( ) => this . refreshOllamaModels ( ) , 5000 )
150+ console . log ( 'retrying list models:' , providerName , error )
151+ const timeoutId = setTimeout ( ( ) => this . refreshModels ( providerName , options ) , REFRESH_INTERVAL )
152+ this . _setTimeoutId ( providerName , timeoutId )
95153 }
96154 } )
97155 }
98156
99- private _setState ( state : RefreshModelState ) {
100- this . state = state
101- this . _onDidChangeState . fire ( )
157+ _clearAllTimeouts ( ) {
158+ for ( const providerName of refreshableProviderNames ) {
159+ this . _clearProviderTimeout ( providerName )
160+ }
161+ }
162+
163+ _clearProviderTimeout ( providerName : RefreshableProviderName ) {
164+ // cancel any existing poll
165+ if ( this . state [ providerName ] . timeoutId ) {
166+ clearTimeout ( this . state [ providerName ] . timeoutId )
167+ this . _setTimeoutId ( providerName , null )
168+ }
169+ }
170+
171+ private _setTimeoutId ( providerName : RefreshableProviderName , timeoutId : NodeJS . Timeout | null ) {
172+ this . state [ providerName ] . timeoutId = timeoutId
173+ }
174+
175+ private _setRefreshState ( providerName : RefreshableProviderName , state : RefreshableState [ 'state' ] ) {
176+ this . state [ providerName ] . state = state
177+ this . _onDidChangeState . fire ( providerName )
102178 }
103179}
104180
0 commit comments