mirror of
https://github.com/voideditor/void
synced 2026-05-24 09:58:23 +00:00
pair programming fix model refresh state
This commit is contained in:
parent
d4fc59034a
commit
8cfe494632
5 changed files with 60 additions and 51 deletions
|
|
@ -25,11 +25,20 @@ type RefreshableState = ({
|
|||
state: 'finished',
|
||||
timeoutId: null,
|
||||
} | {
|
||||
state: 'finished_invisible',
|
||||
state: 'error',
|
||||
timeoutId: null,
|
||||
})
|
||||
|
||||
|
||||
/*
|
||||
|
||||
user click -> error -> fire(error)
|
||||
\> success -> fire(success)
|
||||
finally: keep polling
|
||||
|
||||
poll -> do not fire
|
||||
|
||||
*/
|
||||
export type RefreshModelStateOfProvider = Record<RefreshableProviderName, RefreshableState>
|
||||
|
||||
|
||||
|
|
@ -41,6 +50,8 @@ const refreshBasedOn: { [k in RefreshableProviderName]: (keyof SettingsOfProvide
|
|||
const REFRESH_INTERVAL = 5_000
|
||||
// const COOLDOWN_TIMEOUT = 300
|
||||
|
||||
const autoOptions = { enableProviderOnSuccess: true, doNotFire: true }
|
||||
|
||||
// element-wise equals
|
||||
function eq<T>(a: T[], b: T[]): boolean {
|
||||
if (a.length !== b.length) return false
|
||||
|
|
@ -51,7 +62,7 @@ function eq<T>(a: T[], b: T[]): boolean {
|
|||
}
|
||||
export interface IRefreshModelService {
|
||||
readonly _serviceBrand: undefined;
|
||||
refreshModels: (providerName: RefreshableProviderName) => Promise<void>;
|
||||
startRefreshingModels: (providerName: RefreshableProviderName, options: { enableProviderOnSuccess: boolean, doNotFire: boolean }) => void;
|
||||
onDidChangeState: Event<RefreshableProviderName>;
|
||||
state: RefreshModelStateOfProvider;
|
||||
}
|
||||
|
|
@ -75,7 +86,7 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
|
|||
|
||||
const disposables: Set<IDisposable> = new Set()
|
||||
|
||||
const initializePollingAndOnChange = () => {
|
||||
const initializeAutoPollingAndOnChange = () => {
|
||||
this._clearAllTimeouts()
|
||||
disposables.forEach(d => d.dispose())
|
||||
disposables.clear()
|
||||
|
|
@ -84,14 +95,14 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
|
|||
|
||||
for (const providerName of refreshableProviderNames) {
|
||||
|
||||
const { _enabled: enabled } = this.voidSettingsService.state.settingsOfProvider[providerName]
|
||||
this.refreshModels(providerName, !enabled, { isPolling: true, isInvisible: true })
|
||||
// const { _enabled: enabled } = this.voidSettingsService.state.settingsOfProvider[providerName]
|
||||
this.startRefreshingModels(providerName, autoOptions)
|
||||
|
||||
// every time providerName.enabled changes, refresh models too, like a useEffect
|
||||
let relevantVals = () => refreshBasedOn[providerName].map(settingName => this.voidSettingsService.state.settingsOfProvider[providerName][settingName])
|
||||
let relevantVals = () => refreshBasedOn[providerName].map(settingName => voidSettingsService.state.settingsOfProvider[providerName][settingName])
|
||||
let prevVals = relevantVals() // each iteration of a for loop has its own context and vars, so this is ok
|
||||
disposables.add(
|
||||
this.voidSettingsService.onDidChangeState(() => { // we might want to debounce this
|
||||
voidSettingsService.onDidChangeState(() => { // we might want to debounce this
|
||||
const newVals = relevantVals()
|
||||
if (!eq(prevVals, newVals)) {
|
||||
|
||||
|
|
@ -101,7 +112,7 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
|
|||
// if it was just enabled, or there was a change and it wasn't to the enabled state, refresh
|
||||
if ((enabled && !prevEnabled) || (!enabled && !prevEnabled)) {
|
||||
// if user just clicked enable, refresh
|
||||
this.refreshModels(providerName, !enabled, { isPolling: false, isInvisible: true })
|
||||
this.startRefreshingModels(providerName, autoOptions)
|
||||
}
|
||||
else {
|
||||
// else if user just clicked disable, don't refresh
|
||||
|
|
@ -119,9 +130,9 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
|
|||
|
||||
// on mount (when get init settings state), and if a relevant feature flag changes, start refreshing models
|
||||
voidSettingsService.waitForInitState.then(() => {
|
||||
initializePollingAndOnChange()
|
||||
initializeAutoPollingAndOnChange()
|
||||
this._register(
|
||||
voidSettingsService.onDidChangeState((type) => { if (typeof type === 'object' && type[1] === 'autoRefreshModels') initializePollingAndOnChange() })
|
||||
voidSettingsService.onDidChangeState((type) => { if (typeof type === 'object' && type[1] === 'autoRefreshModels') initializeAutoPollingAndOnChange() })
|
||||
)
|
||||
})
|
||||
|
||||
|
|
@ -129,22 +140,23 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
|
|||
|
||||
state: RefreshModelStateOfProvider = {
|
||||
ollama: { state: 'init', timeoutId: null },
|
||||
// openAICompatible: { state: 'init', timeoutId: null },
|
||||
}
|
||||
|
||||
|
||||
// start listening for models (and don't stop until success)
|
||||
async refreshModels(providerName: RefreshableProviderName, enableProviderOnSuccess?: boolean, options?: { isPolling?: boolean, isInvisible?: boolean }) {
|
||||
|
||||
const { isPolling, isInvisible } = options ?? {}
|
||||
|
||||
// console.log(`refreshModels, isInvisible ${isInvisible} isPolling ${isPolling}`)
|
||||
startRefreshingModels: IRefreshModelService['startRefreshingModels'] = (providerName, options) => {
|
||||
|
||||
this._clearProviderTimeout(providerName)
|
||||
|
||||
// start loading models
|
||||
if (!isInvisible) this._setRefreshState(providerName, 'refreshing')
|
||||
this._setRefreshState(providerName, 'refreshing', options)
|
||||
|
||||
const autoPoll = () => {
|
||||
if (this.voidSettingsService.state.globalSettings.autoRefreshModels) {
|
||||
// resume auto-polling
|
||||
const timeoutId = setTimeout(() => this.startRefreshingModels(providerName, autoOptions), REFRESH_INTERVAL)
|
||||
this._setTimeoutId(providerName, timeoutId)
|
||||
}
|
||||
}
|
||||
const listFn = providerName === 'ollama' ? this.llmMessageService.ollamaList
|
||||
: providerName === 'openAICompatible' ? this.llmMessageService.openAICompatibleList
|
||||
: () => { }
|
||||
|
|
@ -160,34 +172,21 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
|
|||
else if (providerName === 'openAICompatible') return (model as OpenaiCompatibleModelResponse).id;
|
||||
else throw new Error('refreshMode fn: unknown provider', providerName);
|
||||
}),
|
||||
{ enableProviderOnSuccess, isPolling, isInvisible }
|
||||
{ enableProviderOnSuccess: options.enableProviderOnSuccess, hideRefresh: options.doNotFire }
|
||||
)
|
||||
|
||||
// update state
|
||||
if (enableProviderOnSuccess) {
|
||||
this.voidSettingsService.setSettingOfProvider(providerName, '_enabled', true)
|
||||
}
|
||||
|
||||
if (!isInvisible) {
|
||||
this._setRefreshState(providerName, 'finished')
|
||||
} else if (isInvisible) {
|
||||
this._setRefreshState(providerName, 'finished_invisible')
|
||||
}
|
||||
|
||||
if (options.enableProviderOnSuccess) this.voidSettingsService.setSettingOfProvider(providerName, '_enabled', true)
|
||||
|
||||
this._setRefreshState(providerName, 'finished', options)
|
||||
autoPoll()
|
||||
},
|
||||
onError: ({ error }) => {
|
||||
console.log('retrying list models:', providerName, error)
|
||||
this._setRefreshState(providerName, 'error', options)
|
||||
autoPoll()
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
// check if we should poll
|
||||
// if it was originally called as `isPolling` and if the `autoRefreshModels` flag is enabled
|
||||
if (isPolling && this.voidSettingsService.state.globalSettings['autoRefreshModels']) {
|
||||
const timeoutId = setTimeout(() => this.refreshModels(providerName, enableProviderOnSuccess, options), REFRESH_INTERVAL)
|
||||
this._setTimeoutId(providerName, timeoutId)
|
||||
}
|
||||
}
|
||||
|
||||
_clearAllTimeouts() {
|
||||
|
|
@ -208,7 +207,8 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
|
|||
this.state[providerName].timeoutId = timeoutId
|
||||
}
|
||||
|
||||
private _setRefreshState(providerName: RefreshableProviderName, state: RefreshableState['state']) {
|
||||
private _setRefreshState(providerName: RefreshableProviderName, state: RefreshableState['state'], options?: { doNotFire: boolean }) {
|
||||
if (options?.doNotFire) return
|
||||
this.state[providerName].state = state
|
||||
this._onDidChangeState.fire(providerName)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ export interface IVoidSettingsService {
|
|||
setModelSelectionOfFeature: SetModelSelectionOfFeatureFn;
|
||||
setGlobalSetting: SetGlobalSettingFn;
|
||||
|
||||
setAutodetectedModels(providerName: ProviderName, modelNames: string[], logging: { enableProviderOnSuccess?: boolean, isPolling?: boolean, isInvisible?: boolean }): void;
|
||||
setAutodetectedModels(providerName: ProviderName, modelNames: string[], logging: object): void;
|
||||
toggleModelHidden(providerName: ProviderName, modelName: string): void;
|
||||
addModel(providerName: ProviderName, modelName: string): void;
|
||||
deleteModel(providerName: ProviderName, modelName: string): boolean;
|
||||
|
|
@ -223,7 +223,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
|||
|
||||
|
||||
|
||||
setAutodetectedModels(providerName: ProviderName, newDefaultModelNames: string[], logging: { enableProviderOnSuccess?: boolean, isPolling?: boolean, isInvisible?: boolean }) {
|
||||
setAutodetectedModels(providerName: ProviderName, newDefaultModelNames: string[], logging: object) {
|
||||
|
||||
const { models } = this.state.settingsOfProvider[providerName]
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { errorDetails } from '../../../../../../../platform/void/common/llmMessa
|
|||
|
||||
|
||||
export const ErrorDisplay = ({
|
||||
message,
|
||||
message:message_,
|
||||
fullError,
|
||||
onDismiss,
|
||||
showDismiss,
|
||||
|
|
@ -23,6 +23,8 @@ export const ErrorDisplay = ({
|
|||
|
||||
const details = errorDetails(fullError)
|
||||
|
||||
const message = message_ === 'TypeError: fetch failed' ? 'TypeError: fetch failed. This likely means you specified the wrong endpoint in Void Settings.' : message_
|
||||
|
||||
|
||||
return (
|
||||
<div className={`rounded-lg border border-red-200 bg-red-50 p-4 overflow-auto`}>
|
||||
|
|
|
|||
|
|
@ -716,7 +716,7 @@ export const SidebarChat = () => {
|
|||
|
||||
{/* text input */}
|
||||
<VoidInputBox2
|
||||
className='min-h-[81px]'
|
||||
className='min-h-[81px] p-1'
|
||||
placeholder={`${keybindingString} to select. Enter instructions...`}
|
||||
onChangeText={useCallback((newStr: string) => { setInstructionsAreEmpty(!newStr) }, [setInstructionsAreEmpty])}
|
||||
onKeyDown={(e) => {
|
||||
|
|
|
|||
|
|
@ -38,32 +38,39 @@ const RefreshModelButton = ({ providerName }: { providerName: RefreshableProvide
|
|||
const refreshModelService = accessor.get('IRefreshModelService')
|
||||
const metricsService = accessor.get('IMetricsService')
|
||||
|
||||
const [justFinished, setJustFinished] = useState(false)
|
||||
const [justFinished, setJustFinished] = useState<null | 'finished' | 'error'>(null)
|
||||
|
||||
useRefreshModelListener(
|
||||
useCallback((providerName2, refreshModelState) => {
|
||||
if (providerName2 !== providerName) return
|
||||
const { state } = refreshModelState[providerName]
|
||||
if (state !== 'finished') return
|
||||
if (!(state === 'finished' || state === 'error')) return
|
||||
// now we know we just entered 'finished' state for this providerName
|
||||
setJustFinished(true)
|
||||
const tid = setTimeout(() => { setJustFinished(false) }, 2000)
|
||||
setJustFinished(state)
|
||||
const tid = setTimeout(() => { setJustFinished(null) }, 2000)
|
||||
return () => clearTimeout(tid)
|
||||
}, [providerName])
|
||||
)
|
||||
|
||||
const { state } = refreshModelState[providerName]
|
||||
const isRefreshing = state === 'refreshing'
|
||||
|
||||
const { title: providerTitle } = displayInfoOfProviderName(providerName)
|
||||
return <SubtleButton
|
||||
onClick={() => {
|
||||
refreshModelService.refreshModels(providerName)
|
||||
refreshModelService.startRefreshingModels(providerName, { enableProviderOnSuccess: false, doNotFire: false })
|
||||
metricsService.capture('Click', { providerName, action: 'Refresh Models' })
|
||||
}}
|
||||
text={justFinished ? `${providerTitle} Models are up-to-date!` : `Manually refresh models list for ${providerTitle}.`}
|
||||
icon={isRefreshing ? <Loader2 className='size-3 animate-spin' /> : (justFinished ? <Check className='stroke-green-500 size-3' /> : <RefreshCw className='size-3' />)}
|
||||
disabled={isRefreshing || justFinished}
|
||||
text={justFinished === 'finished' ? `${providerTitle} Models are up-to-date!`
|
||||
: justFinished === 'error' ? `${providerTitle} not found!`
|
||||
: `Manually refresh ${providerTitle} models.`
|
||||
}
|
||||
icon={justFinished === 'finished' ? <Check className='stroke-green-500 size-3' />
|
||||
: justFinished === 'error' ? <X className='stroke-red-500 size-3' />
|
||||
: state === 'refreshing' ? <Loader2 className='size-3 animate-spin' />
|
||||
: <RefreshCw className='size-3' />
|
||||
}
|
||||
|
||||
disabled={state === 'refreshing' || justFinished !== null}
|
||||
/>
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue