From 81b78b77656731dfcb41076f556cc4ed961528a2 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Wed, 18 Dec 2024 19:31:50 -0800 Subject: [PATCH] auto fetch model works --- .../void/common/refreshModelService.ts | 128 ++++++++++++------ .../void/common/voidSettingsService.ts | 56 ++++++-- .../platform/void/common/voidSettingsTypes.ts | 89 ++++++++---- .../react/src/void-settings-tsx/Settings.tsx | 50 +++++-- 4 files changed, 231 insertions(+), 92 deletions(-) diff --git a/src/vs/platform/void/common/refreshModelService.ts b/src/vs/platform/void/common/refreshModelService.ts index 9f4df4f8..e552e6d0 100644 --- a/src/vs/platform/void/common/refreshModelService.ts +++ b/src/vs/platform/void/common/refreshModelService.ts @@ -8,7 +8,7 @@ import { InstantiationType, registerSingleton } from '../../instantiation/common import { IVoidSettingsService } from './voidSettingsService.js'; import { ILLMMessageService } from './llmMessageService.js'; import { Emitter, Event } from '../../../base/common/event.js'; -import { Disposable } from '../../../base/common/lifecycle.js'; +import { Disposable, IDisposable } from '../../../base/common/lifecycle.js'; import { ProviderName, SettingsOfProvider } from './voidSettingsTypes.js'; import { OllamaModelResponse, OpenaiCompatibleModelResponse } from './llmMessageTypes.js'; @@ -17,12 +17,27 @@ export const refreshableProviderNames = ['ollama', 'openAICompatible'] satisfies export type RefreshableProviderName = typeof refreshableProviderNames[number] -type ModelRefreshState = 'nothing' | 'refreshing' | 'success' -export type RefreshModelStateOfProvider = Record +type RefreshableState = { + state: 'init', + timeoutId: null, +} | { + state: 'refreshing', + timeoutId: NodeJS.Timeout | null, +} | { + state: 'success', + timeoutId: null, +} + + +export type RefreshModelStateOfProvider = Record + + + +const refreshBasedOn: { [k in RefreshableProviderName]: (keyof SettingsOfProvider[k])[] } = { + ollama: ['enabled', 'endpoint'], + openAICompatible: ['enabled', 'endpoint', 'apiKey'], +} const REFRESH_INTERVAL = 5000 // element-wise equals @@ -55,52 +70,63 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ ) { super() - // on mount, start refreshing models if there are no defaults - const refreshables: { [k in RefreshableProviderName]: (keyof SettingsOfProvider[k])[] } = { - ollama: ['enabled', 'endpoint'], - openAICompatible: ['enabled', 'endpoint', 'apiKey'], + + const disposables: Set = new Set() + + + const startRefreshing = () => { + this._clearAllTimeouts() + disposables.forEach(d => d.dispose()) + disposables.clear() + + if (!voidSettingsService.state.featureFlagSettings.autoRefreshModels) return + + for (const providerName of refreshableProviderNames) { + + const refresh = () => { + // const { enabled } = this.voidSettingsService.state.settingsOfProvider[providerName] + this.refreshModels(providerName, { enableProviderOnSuccess: true }) // enable the provider on success + } + + refresh() + + // every time providerName.enabled changes, refresh models too, like a useEffect + let relevantVals = () => refreshBasedOn[providerName].map(settingName => this.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 + const newVals = relevantVals() + if (!eq(prevVals, newVals)) { + refresh() + prevVals = newVals + } + }) + ) + } } - for (const p in refreshables) { - const providerName = p as keyof typeof refreshables - this.refreshModels(providerName) - - // every time providerName.enabled changes, refresh models too, like useEffect - let relevantVals = () => refreshables[providerName].map(settingName => this.voidSettingsService.state.settingsOfProvider[providerName][settingName]) - let prevVals = relevantVals() // each iteration of a for loop has its own context and vars, so this is ok + // 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 + voidSettingsService.waitForInitState.then(() => { + startRefreshing() this._register( - this.voidSettingsService.onDidChangeState(() => { // we might want to debounce this - const newVals = relevantVals() - if (!eq(prevVals, newVals)) { - this.refreshModels(providerName) - prevVals = newVals - } - }) + voidSettingsService.onDidChangeState((type) => { if (type === 'featureFlagSettings') startRefreshing() }) ) - } + }) } state: RefreshModelStateOfProvider = { - ollama: { state: 'nothing', timeoutId: null }, - openAICompatible: { state: 'nothing', timeoutId: null }, + ollama: { state: 'init', timeoutId: null }, + openAICompatible: { state: 'init', timeoutId: null }, } - async refreshModels(providerName: RefreshableProviderName) { - // cancel any existing poll - if (this.state[providerName].timeoutId) { - clearTimeout(this.state[providerName].timeoutId) - this._setTimeoutId(providerName, null) - } - // if provider is disabled, obivously done - if (!this.voidSettingsService.state.settingsOfProvider[providerName].enabled) { - this._setIsRefreshing(providerName, 'nothing') - return - } + // start listening for models (and don't stop until success) + async refreshModels(providerName: RefreshableProviderName, options?: { enableProviderOnSuccess?: boolean }) { + this._clearProviderTimeout(providerName) // start loading models - this._setIsRefreshing(providerName, 'refreshing') + this._setRefreshState(providerName, 'refreshing') const fn = providerName === 'ollama' ? this.llmMessageService.ollamaList : providerName === 'openAICompatible' ? this.llmMessageService.openAICompatibleList @@ -113,22 +139,40 @@ 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) })) - this._setIsRefreshing(providerName, 'success') + + if (options?.enableProviderOnSuccess) + this.voidSettingsService.setSettingOfProvider(providerName, 'enabled', true) + + this._setRefreshState(providerName, 'success') }, onError: ({ error }) => { // poll console.log('retrying list models:', providerName, error) - const timeoutId = setTimeout(() => this.refreshModels(providerName), REFRESH_INTERVAL) + const timeoutId = setTimeout(() => this.refreshModels(providerName, options), REFRESH_INTERVAL) this._setTimeoutId(providerName, timeoutId) } }) } + _clearAllTimeouts() { + for (const providerName of refreshableProviderNames) { + this._clearProviderTimeout(providerName) + } + } + + _clearProviderTimeout(providerName: RefreshableProviderName) { + // cancel any existing poll + if (this.state[providerName].timeoutId) { + clearTimeout(this.state[providerName].timeoutId) + this._setTimeoutId(providerName, null) + } + } + private _setTimeoutId(providerName: RefreshableProviderName, timeoutId: NodeJS.Timeout | null) { this.state[providerName].timeoutId = timeoutId } - private _setIsRefreshing(providerName: RefreshableProviderName, state: ModelRefreshState) { + private _setRefreshState(providerName: RefreshableProviderName, state: RefreshableState['state']) { this.state[providerName].state = state this._onDidChangeState.fire(providerName) } diff --git a/src/vs/platform/void/common/voidSettingsService.ts b/src/vs/platform/void/common/voidSettingsService.ts index ab9d28f8..0c0ceb28 100644 --- a/src/vs/platform/void/common/voidSettingsService.ts +++ b/src/vs/platform/void/common/voidSettingsService.ts @@ -10,7 +10,7 @@ import { IEncryptionService } from '../../encryption/common/encryptionService.js import { registerSingleton, InstantiationType } from '../../instantiation/common/extensions.js'; import { createDecorator } from '../../instantiation/common/instantiation.js'; import { IStorageService, StorageScope, StorageTarget } from '../../storage/common/storage.js'; -import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, modelInfoOfDefaultNames, ModelInfo } from './voidSettingsTypes.js'; +import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, modelInfoOfDefaultNames, VoidModelInfo, FeatureFlagSettings, FeatureFlagName, defaultFeatureFlagSettings } from './voidSettingsTypes.js'; const STORAGE_KEY = 'void.voidSettingsI' @@ -21,13 +21,13 @@ type SetSettingOfProviderFn = ( newVal: SettingsOfProvider[ProviderName][S extends keyof SettingsOfProvider[ProviderName] ? S : never], ) => Promise; -type SetModelSelectionOfFeature = ( +type SetModelSelectionOfFeatureFn = ( featureName: K, newVal: ModelSelectionOfFeature[K], options?: { doNotApplyEffects?: true } ) => Promise; - +type SetFeatureFlagFn = (flagName: FeatureFlagName, newVal: boolean) => void; export type ModelOption = { text: string, value: ModelSelection } @@ -36,18 +36,24 @@ export type ModelOption = { text: string, value: ModelSelection } export type VoidSettingsState = { readonly settingsOfProvider: SettingsOfProvider; // optionsOfProvider readonly modelSelectionOfFeature: ModelSelectionOfFeature; // stateOfFeature + readonly featureFlagSettings: FeatureFlagSettings; readonly _modelOptions: ModelOption[] // computed based on the two above items } +type EventProp = Exclude | 'all' export interface IVoidSettingsService { readonly _serviceBrand: undefined; - readonly state: VoidSettingsState; - onDidChangeState: Event; + readonly state: VoidSettingsState; // in order to play nicely with react, you should immutably change state + readonly waitForInitState: Promise; + + onDidChangeState: Event; + setSettingOfProvider: SetSettingOfProviderFn; - setModelSelectionOfFeature: SetModelSelectionOfFeature; + setModelSelectionOfFeature: SetModelSelectionOfFeatureFn; + setFeatureFlag: SetFeatureFlagFn; setDefaultModels(providerName: ProviderName, modelNames: string[]): void; toggleModelHidden(providerName: ProviderName, modelName: string): void; @@ -74,6 +80,7 @@ const defaultState = () => { const d: VoidSettingsState = { settingsOfProvider: deepClone(defaultSettingsOfProvider), modelSelectionOfFeature: { 'Ctrl+L': null, 'Ctrl+K': null, 'Autocomplete': null }, + featureFlagSettings: deepClone(defaultFeatureFlagSettings), _modelOptions: _computeModelOptions(defaultSettingsOfProvider), // computed } return d @@ -84,10 +91,11 @@ export const IVoidSettingsService = createDecorator('VoidS class VoidSettingsService extends Disposable implements IVoidSettingsService { _serviceBrand: undefined; - private readonly _onDidChangeState = new Emitter(); - readonly onDidChangeState: Event = this._onDidChangeState.event; // this is primarily for use in react, so react can listen + update on state changes + private readonly _onDidChangeState = new Emitter(); + readonly onDidChangeState: Event = this._onDidChangeState.event; // this is primarily for use in react, so react can listen + update on state changes state: VoidSettingsState; + waitForInitState: Promise // await this if you need a valid state initially constructor( @IStorageService private readonly _storageService: IStorageService, @@ -100,10 +108,14 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { // at the start, we haven't read the partial config yet, but we need to set state to something this.state = defaultState() + let resolver: () => void = () => { } + this.waitForInitState = new Promise((res, rej) => resolver = res) + // read and update the actual state immediately this._readState().then(s => { this.state = s - this._onDidChangeState.fire() + resolver() + this._onDidChangeState.fire('all') }) } @@ -136,6 +148,8 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { } } + const newFeatureFlags = this.state.featureFlagSettings + // if changed models or enabled a provider, recompute models list const modelsListChanged = settingName === 'models' || settingName === 'enabled' const newModelsList = modelsListChanged ? _computeModelOptions(newSettingsOfProvider) : this.state._modelOptions @@ -143,6 +157,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { const newState: VoidSettingsState = { modelSelectionOfFeature: newModelSelectionOfFeature, settingsOfProvider: newSettingsOfProvider, + featureFlagSettings: newFeatureFlags, _modelOptions: newModelsList, } @@ -166,11 +181,26 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { } await this._storeState() - this._onDidChangeState.fire() + this._onDidChangeState.fire('settingsOfProvider') } - setModelSelectionOfFeature: SetModelSelectionOfFeature = async (featureName, newVal, options) => { + setFeatureFlag: SetFeatureFlagFn = async (flagName, newVal) => { + const newState = { + ...this.state, + featureFlagSettings: { + ...this.state.featureFlagSettings, + [flagName]: newVal + } + } + this.state = newState + await this._storeState() + this._onDidChangeState.fire('featureFlagSettings') + + } + + + setModelSelectionOfFeature: SetModelSelectionOfFeatureFn = async (featureName, newVal, options) => { const newState: VoidSettingsState = { ...this.state, modelSelectionOfFeature: { @@ -185,7 +215,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { return await this._storeState() - this._onDidChangeState.fire() + this._onDidChangeState.fire('modelSelectionOfFeature') } @@ -203,7 +233,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { const { models } = this.state.settingsOfProvider[providerName] const modelIdx = models.findIndex(m => m.modelName === modelName) if (modelIdx === -1) return - const newModels: ModelInfo[] = [ + const newModels: VoidModelInfo[] = [ ...models.slice(0, modelIdx), { ...models[modelIdx], isHidden: !models[modelIdx].isHidden }, ...models.slice(modelIdx + 1, Infinity) diff --git a/src/vs/platform/void/common/voidSettingsTypes.ts b/src/vs/platform/void/common/voidSettingsTypes.ts index 893a84b6..7aa1eb78 100644 --- a/src/vs/platform/void/common/voidSettingsTypes.ts +++ b/src/vs/platform/void/common/voidSettingsTypes.ts @@ -7,14 +7,14 @@ -export type ModelInfo = { +export type VoidModelInfo = { modelName: string, isDefault: boolean, // whether or not it's a default for its provider isHidden: boolean, // whether or not the user is hiding it } -export const modelInfoOfDefaultNames = (modelNames: string[]): ModelInfo[] => { +export const modelInfoOfDefaultNames = (modelNames: string[]): VoidModelInfo[] => { const isHidden = modelNames.length >= 10 // hide all models if there are a ton of them, and make user enable them individually return modelNames.map((modelName, i) => ({ modelName, isDefault: true, isHidden })) } @@ -96,7 +96,7 @@ type UnionOfKeys = T extends T ? keyof T : never; -export const customProviderSettingsDefaults = { +export const customProviderSettings = { anthropic: { apiKey: '', }, @@ -121,19 +121,20 @@ export const customProviderSettingsDefaults = { } } as const -export type ProviderName = keyof typeof customProviderSettingsDefaults -export const providerNames = Object.keys(customProviderSettingsDefaults) as ProviderName[] + +export type ProviderName = keyof typeof customProviderSettings +export const providerNames = Object.keys(customProviderSettings) as ProviderName[] -type CustomSettingName = UnionOfKeys +type CustomSettingName = UnionOfKeys type CustomProviderSettings = { - [k in CustomSettingName]: k extends keyof typeof customProviderSettingsDefaults[providerName] ? string : undefined + [k in CustomSettingName]: k extends keyof typeof customProviderSettings[providerName] ? string : undefined } type CommonProviderSettings = { - enabled: boolean, - models: ModelInfo[], // if null, user can type in any string as a model + enabled: boolean | undefined, // undefined initially + models: VoidModelInfo[], } export type SettingsForProvider = CustomProviderSettings & CommonProviderSettings @@ -148,6 +149,14 @@ export type SettingName = keyof SettingsForProvider + +export const customSettingNamesOfProvider = (providerName: ProviderName) => { + return Object.keys(customProviderSettings[providerName]) as CustomSettingName[] +} + + + + export const titleOfProviderName = (providerName: ProviderName) => { if (providerName === 'anthropic') return 'Anthropic' @@ -203,7 +212,7 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName providerName === 'openAICompatible' ? 'baseURL' // (do not include /chat/completions) : '(never)', - placeholder: providerName === 'ollama' ? customProviderSettingsDefaults.ollama.endpoint + placeholder: providerName === 'ollama' ? customProviderSettings.ollama.endpoint : providerName === 'openAICompatible' ? 'https://my-website.com/v1' : '(never)', @@ -267,46 +276,46 @@ export const voidInitModelOptions = { // used when waiting and for a type reference export const defaultSettingsOfProvider: SettingsOfProvider = { anthropic: { + enabled: undefined, ...defaultCustomSettings, - ...customProviderSettingsDefaults.anthropic, + ...customProviderSettings.anthropic, ...voidInitModelOptions.anthropic, - enabled: false, }, openAI: { + enabled: undefined, ...defaultCustomSettings, - ...customProviderSettingsDefaults.openAI, + ...customProviderSettings.openAI, ...voidInitModelOptions.openAI, - enabled: false, }, gemini: { ...defaultCustomSettings, - ...customProviderSettingsDefaults.gemini, + ...customProviderSettings.gemini, ...voidInitModelOptions.gemini, - enabled: false, + enabled: undefined, }, groq: { ...defaultCustomSettings, - ...customProviderSettingsDefaults.groq, + ...customProviderSettings.groq, ...voidInitModelOptions.groq, - enabled: false, + enabled: undefined, }, ollama: { ...defaultCustomSettings, - ...customProviderSettingsDefaults.ollama, + ...customProviderSettings.ollama, ...voidInitModelOptions.ollama, - enabled: false, + enabled: undefined, }, openRouter: { ...defaultCustomSettings, - ...customProviderSettingsDefaults.openRouter, + ...customProviderSettings.openRouter, ...voidInitModelOptions.openRouter, - enabled: false, + enabled: undefined, }, openAICompatible: { ...defaultCustomSettings, - ...customProviderSettingsDefaults.openAICompatible, + ...customProviderSettings.openAICompatible, ...voidInitModelOptions.openAICompatible, - enabled: false, + enabled: undefined, }, } @@ -326,3 +335,35 @@ export type ModelSelectionOfFeature = { export type FeatureName = keyof ModelSelectionOfFeature export const featureNames = ['Ctrl+L', 'Ctrl+K', 'Autocomplete'] as const + + + + + + + + + +export type FeatureFlagSettings = { + autoRefreshModels: boolean; // automatically scan for local models and enable when found +} +export const defaultFeatureFlagSettings: FeatureFlagSettings = { + autoRefreshModels: true, +} + +export type FeatureFlagName = keyof FeatureFlagSettings +export const featureFlagNames = Object.keys(defaultFeatureFlagSettings) as FeatureFlagName[] + +type FeatureFlagDisplayInfo = { + description: string, +} +export const displayInfoOfFeatureFlag = (featureFlag: FeatureFlagName): FeatureFlagDisplayInfo => { + if (featureFlag === 'autoRefreshModels') { + return { + description: 'Automatically scan for and enable local models.', + } + } + throw new Error(`featureFlagInfo: Unknown feature flag: "${featureFlag}"`) +} + + diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx index 6e21e8c5..66b11f3d 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx @@ -1,6 +1,6 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js' -import { ProviderName, SettingName, displayInfoOfSettingName, titleOfProviderName, providerNames, ModelInfo } from '../../../../../../../platform/void/common/voidSettingsTypes.js' +import { ProviderName, SettingName, displayInfoOfSettingName, titleOfProviderName, providerNames, VoidModelInfo, featureFlagNames, displayInfoOfFeatureFlag, customSettingNamesOfProvider } from '../../../../../../../platform/void/common/voidSettingsTypes.js' import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js' import { VoidInputBox, VoidSelectBox } from '../util/inputs.js' import { useIsDark, useRefreshModelListener, useRefreshModelState, useService, useSettingsState } from '../util/services.js' @@ -123,15 +123,15 @@ const AddModelMenu = ({ onSubmit }: { onSubmit: () => void }) => { } -const AddModelButton = () => { +const AddModelMenuFull = () => { const [open, setOpen] = useState(false) - return <> + return
{open ? { setOpen(false) }} /> - : + : } - +
} @@ -141,11 +141,11 @@ export const ModelDump = () => { const settingsState = useSettingsState() // a dump of all the enabled providers' models - const modelDump: (ModelInfo & { providerName: ProviderName, providerEnabled: boolean })[] = [] + const modelDump: (VoidModelInfo & { providerName: ProviderName, providerEnabled: boolean })[] = [] for (let providerName of providerNames) { const providerSettings = settingsState.settingsOfProvider[providerName] // if (!providerSettings.enabled) continue - modelDump.push(...providerSettings.models.map(model => ({ ...model, providerName, providerEnabled: providerSettings.enabled }))) + modelDump.push(...providerSettings.models.map(model => ({ ...model, providerName, providerEnabled: !!providerSettings.enabled }))) } return
@@ -160,7 +160,11 @@ export const ModelDump = () => { {/* right part is anything that fits */}
{isDefault ? '' : '(custom model)'} - +
{isDefault ? null : }
@@ -216,17 +220,16 @@ const SettingsForProvider = ({ providerName }: { providerName: ProviderName }) = const voidSettingsState = useSettingsState() const voidSettingsService = useService('settingsStateService') - const { models, enabled, ...others } = voidSettingsState.settingsOfProvider[providerName] + const { enabled } = voidSettingsState.settingsOfProvider[providerName] + const settingNames = customSettingNamesOfProvider(providerName) return <> -

{titleOfProviderName(providerName)}

{/* settings besides models (e.g. api key) */} - {Object.keys(others).map((sName, i) => { - const settingName = sName as keyof typeof others + {settingNames.map((settingName, i) => { return })} @@ -242,6 +245,26 @@ export const VoidProviderSettings = () => { } +export const VoidFeatureFlagSettings = () => { + const voidSettingsService = useService('settingsStateService') + const voidSettingsState = useSettingsState() + + return <> + {featureFlagNames.map((flagName) => { + const value = voidSettingsState.featureFlagSettings[flagName] + const { description } = displayInfoOfFeatureFlag(flagName) + return
+
+ +

{description}

+
+
+ })} + +} + // full settings @@ -283,7 +306,7 @@ export const Settings = () => {

Models

- +

Providers

@@ -296,6 +319,7 @@ export const Settings = () => {

{ setTab('features') }}>Features

+