add model selection

This commit is contained in:
Andrew Pareles 2024-12-16 00:17:48 -08:00
parent fc187e0028
commit 78a48bae13
12 changed files with 199 additions and 90 deletions

View file

@ -85,7 +85,7 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
this.llmMessageService.ollamaList({
onSuccess: ({ models }) => {
this.voidSettingsService.setSettingOfProvider('ollama', 'models', models.map(model => model.name))
this.voidSettingsService.setDefaultModels('ollama', models.map(model => model.name))
this._setState('done')
},
onError: ({ error }) => {

View file

@ -10,10 +10,10 @@ 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 } from './voidSettingsTypes.js';
import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, modelInfoOfDefaultNames, ModelInfo } from './voidSettingsTypes.js';
const STORAGE_KEY = 'void.voidConfigStateIV'
const STORAGE_KEY = 'void.voidSettings'
type SetSettingOfProviderFn = <S extends SettingName>(
providerName: ProviderName,
@ -37,7 +37,7 @@ export type VoidSettingsState = {
readonly settingsOfProvider: SettingsOfProvider; // optionsOfProvider
readonly modelSelectionOfFeature: ModelSelectionOfFeature; // stateOfFeature
readonly _modelsList: ModelOption[] // computed based on the two above items
readonly _modelOptions: ModelOption[] // computed based on the two above items
}
@ -48,19 +48,25 @@ export interface IVoidSettingsService {
onDidChangeState: Event<void>;
setSettingOfProvider: SetSettingOfProviderFn;
setModelSelectionOfFeature: SetModelSelectionOfFeature;
setDefaultModels(providerName: ProviderName, modelNames: string[]): void;
toggleModelHidden(providerName: ProviderName, modelName: string): void;
addModel(providerName: ProviderName, modelName: string): void;
deleteModel(providerName: ProviderName, modelName: string): boolean;
}
let _computeModelsList = (settingsOfProvider: SettingsOfProvider) => {
let modelsList: ModelOption[] = []
let _computeModelOptions = (settingsOfProvider: SettingsOfProvider) => {
let modelOptions: ModelOption[] = []
for (const providerName of providerNames) {
const providerConfig = settingsOfProvider[providerName]
if (providerConfig.enabled !== 'true') continue
providerConfig.models?.forEach(modelName => {
modelsList.push({ text: `${modelName} (${providerName})`, value: { providerName, modelName } })
})
for (const { modelName, isHidden } of providerConfig.models) {
if (isHidden) continue
modelOptions.push({ text: `${modelName} (${providerName})`, value: { providerName, modelName } })
}
}
return modelsList
return modelOptions
}
@ -68,7 +74,7 @@ const defaultState = () => {
const d: VoidSettingsState = {
settingsOfProvider: deepClone(defaultSettingsOfProvider),
modelSelectionOfFeature: { 'Ctrl+L': null, 'Ctrl+K': null, 'Autocomplete': null },
_modelsList: _computeModelsList(defaultSettingsOfProvider),
_modelOptions: _computeModelOptions(defaultSettingsOfProvider), // computed
}
return d
}
@ -132,12 +138,12 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
// if changed models or enabled a provider, recompute models list
const modelsListChanged = settingName === 'models' || settingName === 'enabled'
const newModelsList = modelsListChanged ? _computeModelsList(newSettingsOfProvider) : this.state._modelsList
const newModelsList = modelsListChanged ? _computeModelOptions(newSettingsOfProvider) : this.state._modelOptions
const newState: VoidSettingsState = {
modelSelectionOfFeature: newModelSelectionOfFeature,
settingsOfProvider: newSettingsOfProvider,
_modelsList: newModelsList,
_modelOptions: newModelsList,
}
// this must go above this.setanythingelse()
@ -177,7 +183,6 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
if (options?.doNotApplyEffects)
return
console.log('NEW STATE II', JSON.stringify(newState, null, 2))
await this._storeState()
this._onDidChangeState.fire()
@ -185,6 +190,48 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
setDefaultModels(providerName: ProviderName, newDefaultModelNames: string[]) {
const { models } = this.state.settingsOfProvider[providerName]
const newDefaultModels = modelInfoOfDefaultNames(newDefaultModelNames)
const newModels = [
...newDefaultModels,
...models.filter(m => !m.isDefault), // keep any non-default models
]
this.setSettingOfProvider(providerName, 'models', newModels)
}
toggleModelHidden(providerName: ProviderName, modelName: string) {
const { models } = this.state.settingsOfProvider[providerName]
const modelIdx = models.findIndex(m => m.modelName === modelName)
if (modelIdx === -1) return
const newModels: ModelInfo[] = [
...models.slice(0, modelIdx),
{ ...models[modelIdx], isHidden: !models[modelIdx].isHidden },
...models.slice(modelIdx + 1, Infinity)
]
this.setSettingOfProvider(providerName, 'models', newModels)
}
addModel(providerName: ProviderName, modelName: string) {
const { models } = this.state.settingsOfProvider[providerName]
const existingIdx = models.findIndex(m => m.modelName === modelName)
if (existingIdx !== -1) return // if exists, do nothing
const newModels = [
...models,
{ modelName, isDefault: false, isHidden: false }
]
this.setSettingOfProvider(providerName, 'models', newModels)
}
deleteModel(providerName: ProviderName, modelName: string): boolean {
const { models } = this.state.settingsOfProvider[providerName]
const delIdx = models.findIndex(m => m.modelName === modelName)
if (delIdx === -1) return false
const newModels = [
...models.slice(0, delIdx), // delete the idx
...models.slice(delIdx + 1, Infinity)
]
this.setSettingOfProvider(providerName, 'models', newModels)
return true
}
}

View file

@ -5,17 +5,30 @@
*--------------------------------------------------------------------------------------------*/
export type ModelInfo = {
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[] => {
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 }))
}
// https://docs.anthropic.com/en/docs/about-claude/models
export const defaultAnthropicModels = [
export const defaultAnthropicModels = modelInfoOfDefaultNames([
'claude-3-5-sonnet-20241022',
'claude-3-5-haiku-20241022',
'claude-3-opus-20240229',
'claude-3-sonnet-20240229',
// 'claude-3-haiku-20240307',
]
])
export const defaultOpenAIModels = [
// https://platform.openai.com/docs/models/gp
export const defaultOpenAIModels = modelInfoOfDefaultNames([
'o1-preview',
'o1-mini',
'gpt-4o',
@ -33,23 +46,24 @@ export const defaultOpenAIModels = [
// 'gpt-3.5-turbo-0125',
// 'gpt-3.5-turbo',
// 'gpt-3.5-turbo-1106',
]
])
export const defaultGroqModels = [
// https://console.groq.com/docs/models
export const defaultGroqModels = modelInfoOfDefaultNames([
"mixtral-8x7b-32768",
"llama2-70b-4096",
"gemma-7b-it"
]
])
export const defaultGeminiModels = [
export const defaultGeminiModels = modelInfoOfDefaultNames([
'gemini-1.5-flash',
'gemini-1.5-pro',
'gemini-1.5-flash-8b',
'gemini-1.0-pro'
]
])
@ -152,7 +166,7 @@ export type SettingsOfProvider = {
{
enabled: string, // 'true' | 'false'
models: string[], // if null, user can type in any string as a model
models: ModelInfo[], // if null, user can type in any string as a model
})
}
@ -245,6 +259,16 @@ export const defaultSettingsOfProvider: SettingsOfProvider = {
...voidInitModelOptions.openAI,
enabled: 'false',
},
gemini: {
...voidProviderDefaults.gemini,
...voidInitModelOptions.gemini,
enabled: 'false',
},
groq: {
...voidProviderDefaults.groq,
...voidInitModelOptions.groq,
enabled: 'false',
},
ollama: {
...voidProviderDefaults.ollama,
...voidInitModelOptions.ollama,
@ -260,16 +284,6 @@ export const defaultSettingsOfProvider: SettingsOfProvider = {
...voidInitModelOptions.openAICompatible,
enabled: 'false',
},
gemini: {
...voidProviderDefaults.gemini,
...voidInitModelOptions.gemini,
enabled: 'false',
},
groq: {
...voidProviderDefaults.groq,
...voidInitModelOptions.groq,
enabled: 'false',
}
}

View file

@ -15,8 +15,6 @@ import { useSidebarState } from '../util/services.js';
import '../styles.css'
import { SidebarThreadSelector } from './SidebarThreadSelector.js';
import { SidebarChat } from './SidebarChat.js';
import { ModelSelectionSettings } from '../void-settings-tsx/ModelSelectionSettings.js';
import { VoidProviderSettings } from '../void-settings-tsx/VoidProviderSettings.js';
import ErrorBoundary from './ErrorBoundary.js';
export const Sidebar = () => {

View file

@ -22,7 +22,7 @@ import { OnError, ServiceSendLLMMessageParams } from '../../../../../../../platf
import { getCmdKey } from '../../../helpers/getCmdKey.js'
import { HistoryInputBox, InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js';
import { VoidInputBox } from './inputs.js';
import { ModelSelectionOfFeature } from '../void-settings-tsx/ModelSelectionSettings.js';
import { ModelDropdown } from '../void-settings-tsx/ModelDropdown.js';
const IconX = ({ size, className = '' }: { size: number, className?: string }) => {
@ -487,7 +487,7 @@ export const SidebarChat = () => {
<div className='flex flex-row justify-between items-end'>
{/* submit options */}
<div>
<ModelSelectionOfFeature featureName='Ctrl+L' />
<ModelDropdown featureName='Ctrl+L' />
</div>
{/* submit / stop button */}

View file

@ -10,14 +10,14 @@ import { VoidSelectBox } from '../sidebar-tsx/inputs.js'
import { SelectBox } from '../../../../../../../base/browser/ui/selectBox/selectBox.js'
const SelectBoxOfFeature = ({ featureName }: { featureName: FeatureName }) => {
const ModelSelectBox = ({ featureName }: { featureName: FeatureName }) => {
const voidSettingsService = useService('settingsStateService')
const settingsState = useSettingsState()
let weChangedText = false
return <VoidSelectBox
options={settingsState._modelsList}
options={settingsState._modelOptions}
onChangeSelection={useCallback((newVal: ModelSelection) => {
if (weChangedText) return
voidSettingsService.setModelSelectionOfFeature(featureName, newVal)
@ -25,7 +25,7 @@ const SelectBoxOfFeature = ({ featureName }: { featureName: FeatureName }) => {
// we are responsible for setting the initial state here. always sync instance when state changes.
onCreateInstance={useCallback((instance: SelectBox) => {
const syncInstance = () => {
const modelsListRef = voidSettingsService.state._modelsList // as a ref
const modelsListRef = voidSettingsService.state._modelOptions // as a ref
const settingsAtProvider = voidSettingsService.state.modelSelectionOfFeature[featureName]
const selectionIdx = settingsAtProvider === null ? -1 : modelsListRef.findIndex(v => modelSelectionsEqual(v.value, settingsAtProvider))
weChangedText = true
@ -39,34 +39,17 @@ const SelectBoxOfFeature = ({ featureName }: { featureName: FeatureName }) => {
/>
}
const DummySelectBox = () => {
return <VoidSelectBox
options={[{ text: 'Please add a model!', value: null }]}
onChangeSelection={() => { }}
onCreateInstance={() => { }}
/>
}
export const ModelSelectionOfFeature = ({ featureName }: { featureName: FeatureName }) => {
export const ModelDropdown = ({ featureName }: { featureName: FeatureName }) => {
const settingsState = useSettingsState()
return <>
{settingsState._modelsList.length === 0 ? 'Please add a provider!' : <SelectBoxOfFeature featureName={featureName} />}
{settingsState._modelOptions.length === 0 ? <DummySelectBox /> : <ModelSelectBox featureName={featureName} />}
</>
}
const RefreshModels = () => {
const refreshModelState = useRefreshModelState()
const refreshModelService = useService('refreshModelService')
return <>
<button onClick={() => refreshModelService.refreshOllamaModels()}>
refresh
</button>
{refreshModelState === 'loading' ? 'loading...' : '✅'}
</>
}
export const ModelSelectionSettings = () => {
return <>
{featureNames.map(featureName => <ModelSelectionOfFeature
key={featureName}
featureName={featureName}
/>)}
<RefreshModels />
</>
}

View file

@ -0,0 +1,58 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
import { ModelInfo, ProviderName, providerNames } from '../../../../../../../platform/void/common/voidSettingsTypes.js'
import { useRefreshModelState, useService, useSettingsState } from '../util/services.js'
const Refreshables = () => {
const settingsState = useSettingsState()
const refreshModelState = useRefreshModelState()
const refreshModelService = useService('refreshModelService')
if (settingsState.settingsOfProvider.ollama.enabled !== 'true')
return null
return <>
<button onClick={() => refreshModelService.refreshOllamaModels()}>refresh Ollama built-in models</button>
{refreshModelState === 'loading' ? 'loading...' : 'good!'}
</>
}
export const ModelMenu = () => {
const settingsStateService = useService('settingsStateService')
const settingsState = useSettingsState()
// a dump of all the enabled providers' models
const models: (ModelInfo & { providerName: ProviderName })[] = []
for (let providerName of providerNames) {
const providerSettings = settingsState.settingsOfProvider[providerName]
if (providerSettings.enabled !== 'true') continue
models.push(...providerSettings.models.map(model => ({ ...model, providerName })))
}
return <>
{models.map(m => {
const { isHidden, isDefault, modelName, providerName } = m
return <div key={`${modelName}${providerName}`} className='flex items-center justify-between gap-4'>
<span>{modelName} {isDefault ? '' : '(custom)'}</span>
<span>{providerName}</span>
<span onClick={() => { settingsStateService.toggleModelHidden(providerName, modelName) }}>{isHidden ? 'hidden' : '✅'}</span>
</div>
})}
<Refreshables />
</>
}

View file

@ -0,0 +1,17 @@
import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js'
import { ModelMenu } from './ModelSettings.js'
import { VoidProviderSettings } from './ProviderSettings.js'
export const Settings = () => {
return <div className='@@void-scope w-full h-full'>
<ErrorBoundary>
<ModelMenu />
</ErrorBoundary>
<ErrorBoundary>
<VoidProviderSettings />
</ErrorBoundary>
</div>
}

View file

@ -1,13 +0,0 @@
import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js'
import { VoidProviderSettings } from './VoidProviderSettings.js'
export const VoidSettings = () => {
return <>
<ErrorBoundary>
<VoidProviderSettings />
</ErrorBoundary>
</>
}

View file

@ -1,6 +1,6 @@
import { mountFnGenerator } from '../util/mountFnGenerator.js'
import { VoidSettings } from './VoidSettings.js'
import { Settings } from './Settings.js'
export const mountVoidSettings = mountFnGenerator(VoidSettings)
export const mountVoidSettings = mountFnGenerator(Settings)

View file

@ -26,11 +26,12 @@ import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextke
import { mountVoidSettings } from './react/out/void-settings-tsx/index.js'
import { getReactServices } from './helpers/reactServicesHelper.js';
import { Codicon } from '../../../../base/common/codicons.js';
import { IDisposable } from '../../../../base/common/lifecycle.js';
// refer to preferences.contribution.ts keybindings editor
class VoidEditorInput extends EditorInput {
class VoidSettingsInput extends EditorInput {
static readonly ID: string = 'workbench.input.void.settings';
@ -44,7 +45,7 @@ class VoidEditorInput extends EditorInput {
}
override get typeId(): string {
return VoidEditorInput.ID;
return VoidSettingsInput.ID;
}
override getName(): string {
@ -54,7 +55,7 @@ class VoidEditorInput extends EditorInput {
}
class MyCustomPane extends EditorPane {
class VoidSettingsPane extends EditorPane {
static readonly ID = 'workbench.test.myCustomPane';
constructor(
@ -64,14 +65,18 @@ class MyCustomPane extends EditorPane {
@IStorageService storageService: IStorageService,
@IInstantiationService private readonly instantiationService: IInstantiationService
) {
super(MyCustomPane.ID, group, telemetryService, themeService, storageService);
super(VoidSettingsPane.ID, group, telemetryService, themeService, storageService);
}
protected createEditor(container: HTMLElement): void {
protected createEditor(parent: HTMLElement): void {
parent.style.overflow = 'auto'
parent.style.userSelect = 'text'
// gets set immediately
this.instantiationService.invokeFunction(accessor => {
const services = getReactServices(accessor)
mountVoidSettings(container, services);
const disposables: IDisposable[] | undefined = mountVoidSettings(parent, services);
disposables?.forEach(d => this._register(d))
})
}
@ -87,8 +92,8 @@ class MyCustomPane extends EditorPane {
Registry.as<IEditorPaneRegistry>(EditorExtensions.EditorPane).registerEditorPane(
EditorPaneDescriptor.create(MyCustomPane, MyCustomPane.ID, nls.localize('MyCustomPane', "CustomPane")),
[new SyncDescriptor(VoidEditorInput)]
EditorPaneDescriptor.create(VoidSettingsPane, VoidSettingsPane.ID, nls.localize('VoidSettingsPane', "Void Settings Pane")),
[new SyncDescriptor(VoidSettingsInput)]
);
@ -117,7 +122,7 @@ registerAction2(class extends Action2 {
async run(accessor: ServicesAccessor): Promise<void> {
const editorService = accessor.get(IEditorService);
const instantiationService = accessor.get(IInstantiationService);
const input = instantiationService.createInstance(VoidEditorInput);
const input = instantiationService.createInstance(VoidSettingsInput);
await editorService.openEditor(input);
}
})