auto fetch model works

This commit is contained in:
Andrew Pareles 2024-12-18 19:31:50 -08:00
parent c3c53b5e81
commit 81b78b7765
4 changed files with 231 additions and 92 deletions

View file

@ -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<RefreshableProviderName, {
state: ModelRefreshState,
timeoutId: NodeJS.Timeout | null // not really part of state
}>
type RefreshableState = {
state: 'init',
timeoutId: null,
} | {
state: 'refreshing',
timeoutId: NodeJS.Timeout | null,
} | {
state: 'success',
timeoutId: null,
}
export type RefreshModelStateOfProvider = Record<RefreshableProviderName, RefreshableState>
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<IDisposable> = 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)
}

View file

@ -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 = <S extends SettingName>(
newVal: SettingsOfProvider[ProviderName][S extends keyof SettingsOfProvider[ProviderName] ? S : never],
) => Promise<void>;
type SetModelSelectionOfFeature = <K extends FeatureName>(
type SetModelSelectionOfFeatureFn = <K extends FeatureName>(
featureName: K,
newVal: ModelSelectionOfFeature[K],
options?: { doNotApplyEffects?: true }
) => Promise<void>;
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<keyof VoidSettingsState, '_modelOptions'> | 'all'
export interface IVoidSettingsService {
readonly _serviceBrand: undefined;
readonly state: VoidSettingsState;
onDidChangeState: Event<void>;
readonly state: VoidSettingsState; // in order to play nicely with react, you should immutably change state
readonly waitForInitState: Promise<void>;
onDidChangeState: Event<EventProp>;
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<IVoidSettingsService>('VoidS
class VoidSettingsService extends Disposable implements IVoidSettingsService {
_serviceBrand: undefined;
private readonly _onDidChangeState = new Emitter<void>();
readonly onDidChangeState: Event<void> = this._onDidChangeState.event; // this is primarily for use in react, so react can listen + update on state changes
private readonly _onDidChangeState = new Emitter<EventProp>();
readonly onDidChangeState: Event<EventProp> = this._onDidChangeState.event; // this is primarily for use in react, so react can listen + update on state changes
state: VoidSettingsState;
waitForInitState: Promise<void> // 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)

View file

@ -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> = 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<typeof customProviderSettingsDefaults[ProviderName]>
type CustomSettingName = UnionOfKeys<typeof customProviderSettings[ProviderName]>
type CustomProviderSettings<providerName extends ProviderName> = {
[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<providerName extends ProviderName> = CustomProviderSettings<providerName> & CommonProviderSettings
@ -148,6 +149,14 @@ export type SettingName = keyof SettingsForProvider<ProviderName>
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}"`)
}

View file

@ -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 <div className='my-2 hover:bg-black/10 dark:hover:bg-gray-200/10 py-1 px-3 rounded-sm overflow-hidden '>
{open ?
<AddModelMenu onSubmit={() => { setOpen(false) }} />
: <button onClick={() => setOpen(true)}>Add Model</button>
: <button className='' onClick={() => setOpen(true)}>Add Model</button>
}
</>
</div>
}
@ -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 <div className=''>
@ -160,7 +160,11 @@ export const ModelDump = () => {
{/* right part is anything that fits */}
<div className='w-fit flex items-center gap-4'>
<span className='opacity-50 whitespace-nowrap'>{isDefault ? '' : '(custom model)'}</span>
<button disabled={!providerEnabled} onClick={() => { settingsStateService.toggleModelHidden(providerName, modelName) }}>{(!providerEnabled || isHidden) ? '❌' : '✅'}</button>
<button disabled={!providerEnabled} onClick={() => { settingsStateService.toggleModelHidden(providerName, modelName) }}>
{!providerEnabled ? '🌑' // provider disabled
: isHidden ? '❌' // model is disabled
: '✅'}
</button>
<div className='w-5 flex items-center justify-center'>
{isDefault ? null : <button onClick={() => { settingsStateService.deleteModel(providerName, modelName) }}><X className='size-4' /></button>}
</div>
@ -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 <>
<div className='flex items-center gap-4'>
<h3 className='text-xl'>{titleOfProviderName(providerName)}</h3>
<button onClick={() => { voidSettingsService.setSettingOfProvider(providerName, 'enabled', !enabled) }}>{enabled ? '✅' : '❌'}</button>
</div>
{/* 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 <ProviderSetting key={settingName} providerName={providerName} settingName={settingName} />
})}
</>
@ -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 <div key={flagName} className='hover:bg-black/10 hover:dark:bg-gray-200/10 rounded-sm overflow-hidden py-1 px-3 my-1'>
<div className='flex items-center gap-4'>
<button onClick={() => { voidSettingsService.setFeatureFlag(flagName, !value) }}>
{value ? '✅' : '❌'}
</button>
<h4 className='text-sm'>{description}</h4>
</div>
</div>
})}
</>
}
// full settings
@ -283,7 +306,7 @@ export const Settings = () => {
<h2 className={`text-3xl mb-2`}>Models</h2>
<ErrorBoundary>
<ModelDump />
<AddModelButton />
<AddModelMenuFull />
<RefreshableModels />
</ErrorBoundary>
<h2 className={`text-3xl mt-4 mb-2`}>Providers</h2>
@ -296,6 +319,7 @@ export const Settings = () => {
<div className={`${tab !== 'features' ? 'hidden' : ''}`}>
<h2 className={`text-3xl mb-2`} onClick={() => { setTab('features') }}>Features</h2>
<VoidFeatureFlagSettings />
</div>
</div>