mirror of
https://github.com/shane-lamb/obsidian-llm-docs.git
synced 2026-07-22 05:44:55 +00:00
added selector for default model
Added a button to choose the default model from a list of available models, in the settings tab. The list is populated from cache.
This commit is contained in:
parent
ab77edc24e
commit
9505cf1bfb
8 changed files with 107 additions and 53 deletions
|
|
@ -1,17 +1,44 @@
|
|||
import { LlmConnectionSettings } from './settings'
|
||||
import { getAvailableOpenaiModels } from './open-ai'
|
||||
import { modelCacheUpdated } from './registry'
|
||||
|
||||
const modelToConnectionIdCache = new Map<string, string>()
|
||||
const modelToConnectionCache = new Map<string, string>()
|
||||
|
||||
function getConnectionId(connection: LlmConnectionSettings) {
|
||||
export interface ConnectionModel {
|
||||
connection: LlmConnectionSettings
|
||||
model: string
|
||||
}
|
||||
|
||||
export function getCachedConnectionModels(connections: LlmConnectionSettings[]): ConnectionModel[] {
|
||||
const models: ConnectionModel[] = []
|
||||
for (const connection of connections) {
|
||||
const connectionId = getConnectionId(connection)
|
||||
for (const [model, id] of modelToConnectionCache) {
|
||||
if (id === connectionId) {
|
||||
models.push({ connection, model })
|
||||
}
|
||||
}
|
||||
}
|
||||
return models
|
||||
}
|
||||
|
||||
export function getConnectionId(connection: LlmConnectionSettings) {
|
||||
return connection.type + connection.baseUrl
|
||||
}
|
||||
|
||||
export async function getAvailableModelsAndUpdateCache(connection: LlmConnectionSettings) {
|
||||
let models = await getAvailableOpenaiModels(connection)
|
||||
for (const model of models) {
|
||||
modelToConnectionCache.set(model, getConnectionId(connection))
|
||||
}
|
||||
modelCacheUpdated.emit('change')
|
||||
}
|
||||
|
||||
function resolveCachedConnectionForModel(
|
||||
connections: LlmConnectionSettings[],
|
||||
model: string,
|
||||
): LlmConnectionSettings | null {
|
||||
const cachedConnectionId = modelToConnectionIdCache.get(model)
|
||||
const cachedConnectionId = modelToConnectionCache.get(model)
|
||||
if (cachedConnectionId) {
|
||||
const matching = connections.find((connection) => getConnectionId(connection) === cachedConnectionId)
|
||||
if (matching) {
|
||||
|
|
@ -30,18 +57,7 @@ export async function resolveConnectionForModel(
|
|||
return cached
|
||||
}
|
||||
|
||||
const promises = connections.map((connection) =>
|
||||
getAvailableOpenaiModels(connection)
|
||||
.then((models) => {
|
||||
if (!models) {
|
||||
return
|
||||
}
|
||||
for (const model of models) {
|
||||
modelToConnectionIdCache.set(model, getConnectionId(connection))
|
||||
}
|
||||
})
|
||||
.catch(() => {}),
|
||||
)
|
||||
const promises = connections.map((connection) => getAvailableModelsAndUpdateCache(connection).catch(() => {}))
|
||||
await Promise.all(promises)
|
||||
|
||||
return resolveCachedConnectionForModel(connections, model)
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export interface OpenaiContent {
|
|||
}
|
||||
}
|
||||
|
||||
export async function getAvailableOpenaiModels(settings: LlmConnectionSettings): Promise<string[] | undefined> {
|
||||
export async function getAvailableOpenaiModels(settings: LlmConnectionSettings): Promise<string[]> {
|
||||
const response = await fetch(`${settings.baseUrl}/v1/models`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
|
@ -33,7 +33,7 @@ export async function getAvailableOpenaiModels(settings: LlmConnectionSettings):
|
|||
await throwOnBadResponse(response)
|
||||
|
||||
const data: any = await response.json()
|
||||
return data?.data.map((model: any) => model.id)
|
||||
return data.data.map((model: any) => model.id)
|
||||
}
|
||||
|
||||
export class OpenaiChatCompletionStream extends EventEmitter {
|
||||
|
|
|
|||
|
|
@ -25,3 +25,7 @@ export interface ILlmDocsPlugin {
|
|||
let llmDocsPlugin: ILlmDocsPlugin
|
||||
export const getLlmDocsPlugin = () => llmDocsPlugin
|
||||
export const setLlmDocsPlugin = (plugin: ILlmDocsPlugin) => (llmDocsPlugin = plugin)
|
||||
|
||||
export const modelCacheUpdated = new EventEmitter()
|
||||
|
||||
export const modelSelected = new EventEmitter()
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Notice, Setting } from 'obsidian'
|
||||
import { getAvailableOpenaiModels } from '../open-ai'
|
||||
import LlmDocsPlugin from '../main'
|
||||
import { getAvailableModelsAndUpdateCache } from '../connection-models'
|
||||
|
||||
export function addConnectionsSettings(containerEl: HTMLElement, plugin: LlmDocsPlugin, redraw: () => void) {
|
||||
new Setting(containerEl)
|
||||
|
|
@ -49,7 +49,7 @@ export function addConnectionsSettings(containerEl: HTMLElement, plugin: LlmDocs
|
|||
button.setButtonText('Test').onClick(async () => {
|
||||
button.setDisabled(true)
|
||||
try {
|
||||
await getAvailableOpenaiModels(connection)
|
||||
await getAvailableModelsAndUpdateCache(connection)
|
||||
new Notice('Connection success!')
|
||||
} catch (error) {
|
||||
new Notice(error)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import { App, PluginSettingTab, Setting, TextComponent } from 'obsidian'
|
||||
import { DocOpenMethods, openaiModels } from '../settings'
|
||||
import { DocOpenMethods } from '../settings'
|
||||
import LlmDocsPlugin from '../main'
|
||||
import { addConnectionsSettings } from './connections'
|
||||
import { modelCacheUpdated, modelSelected } from '../registry'
|
||||
import { ModelPickerModal } from './model-picker'
|
||||
|
||||
export class SettingsTab extends PluginSettingTab {
|
||||
plugin: LlmDocsPlugin
|
||||
|
|
@ -9,6 +11,13 @@ export class SettingsTab extends PluginSettingTab {
|
|||
constructor(app: App, plugin: LlmDocsPlugin) {
|
||||
super(app, plugin)
|
||||
this.plugin = plugin
|
||||
modelCacheUpdated.on('change', () => this.display())
|
||||
}
|
||||
|
||||
hide() {
|
||||
super.hide()
|
||||
modelCacheUpdated.removeAllListeners()
|
||||
modelSelected.removeAllListeners()
|
||||
}
|
||||
|
||||
display(): void {
|
||||
|
|
@ -67,38 +76,27 @@ export class SettingsTab extends PluginSettingTab {
|
|||
}
|
||||
|
||||
addDefaultModelSetting(containerEl: HTMLElement) {
|
||||
const initialValue = this.plugin.settings.defaults.model
|
||||
const initialValueIsOther = !Object.keys(openaiModels).includes(initialValue)
|
||||
let textComponent: TextComponent
|
||||
|
||||
const save = async (value: string) => {
|
||||
this.plugin.settings.defaults.model = value
|
||||
await this.plugin.saveSettings()
|
||||
}
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Default model')
|
||||
.setDesc('The default LLM model variant to use for new LLM documents')
|
||||
.addDropdown((dropdown) => {
|
||||
dropdown
|
||||
.addOptions({
|
||||
...openaiModels,
|
||||
other: 'Custom',
|
||||
})
|
||||
.setValue(initialValueIsOther ? 'other' : initialValue)
|
||||
.onChange(async (value) => {
|
||||
if (value === 'other') {
|
||||
textComponent.setDisabled(false)
|
||||
textComponent.inputEl.focus()
|
||||
} else {
|
||||
textComponent.setDisabled(true)
|
||||
textComponent.setValue(value)
|
||||
this.plugin.settings.defaults.model = value
|
||||
await this.plugin.saveSettings()
|
||||
}
|
||||
.addButton((button) => {
|
||||
button.setButtonText('Select').onClick(() => {
|
||||
new ModelPickerModal(this.app, this.plugin.settings.connections).open()
|
||||
modelSelected.on('change', async (model) => {
|
||||
textComponent.setValue(model)
|
||||
await save(model)
|
||||
})
|
||||
})
|
||||
})
|
||||
.addText((text) => {
|
||||
text.setValue(initialValue)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.defaults.model = value
|
||||
await this.plugin.saveSettings()
|
||||
})
|
||||
.setDisabled(!initialValueIsOther)
|
||||
text.setValue(this.plugin.settings.defaults.model).onChange(async (value) => save(value))
|
||||
textComponent = text
|
||||
})
|
||||
}
|
||||
|
|
|
|||
35
src/settings-tab/model-picker.ts
Normal file
35
src/settings-tab/model-picker.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import { App, SuggestModal } from 'obsidian'
|
||||
import { LlmConnectionSettings } from '../settings'
|
||||
import { ConnectionModel, getCachedConnectionModels, getConnectionId } from '../connection-models'
|
||||
import { modelSelected } from '../registry'
|
||||
|
||||
export class ModelPickerModal extends SuggestModal<ConnectionModel> {
|
||||
private models: ConnectionModel[]
|
||||
|
||||
constructor(app: App, connections: LlmConnectionSettings[]) {
|
||||
super(app)
|
||||
const collator = new Intl.Collator()
|
||||
this.models = getCachedConnectionModels(connections).sort((a, b) => collator.compare(sortKey(a), sortKey(b)))
|
||||
this.emptyStateText = 'No results. Click "Test" on each connection to populate the list of models.'
|
||||
}
|
||||
|
||||
getSuggestions(query: string): ConnectionModel[] {
|
||||
return this.models.filter((model) => model.model.toLowerCase().includes(query.toLowerCase()))
|
||||
}
|
||||
|
||||
renderSuggestion({ connection, model }: ConnectionModel, el: HTMLElement) {
|
||||
el.createEl('div', { text: model, cls: 'llmdocs-suggest-item-title' })
|
||||
el.createEl('small', {
|
||||
text: `${connection.type} @ ${connection.baseUrl}`,
|
||||
cls: 'llmdocs-suggest-item-subtext',
|
||||
})
|
||||
}
|
||||
|
||||
onChooseSuggestion(model: ConnectionModel, evt: MouseEvent | KeyboardEvent) {
|
||||
modelSelected.emit('change', model.model)
|
||||
}
|
||||
}
|
||||
|
||||
function sortKey(model: ConnectionModel): string {
|
||||
return getConnectionId(model.connection) + '_' + model.model
|
||||
}
|
||||
|
|
@ -18,24 +18,17 @@ export interface DefaultsSettings {
|
|||
docOpenMethod: DocOpenMethods
|
||||
}
|
||||
|
||||
export const openaiModels = {
|
||||
'gpt-4o': 'GPT-4o',
|
||||
'gpt-4o-mini': 'GPT-4o mini',
|
||||
}
|
||||
|
||||
export interface LlmConnectionSettings {
|
||||
type: 'OpenAI' // 'Anthropic' and other types in future
|
||||
baseUrl: string
|
||||
apiKey: string
|
||||
}
|
||||
|
||||
const defaultModel: keyof typeof openaiModels = 'gpt-4o'
|
||||
|
||||
export const defaultPluginSettings: PluginSettings = {
|
||||
docsDir: 'LLM',
|
||||
connections: [],
|
||||
defaults: {
|
||||
model: defaultModel,
|
||||
model: 'gpt-4o',
|
||||
systemPrompt: '',
|
||||
docOpenMethod: DocOpenMethods.tab,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -58,3 +58,11 @@ If your plugin does not need CSS, delete this file.
|
|||
opacity: 50%;
|
||||
animation: rotate90 2s steps(1) infinite;
|
||||
}
|
||||
|
||||
.llmdocs-suggest-item-title {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.llmdocs-suggest-item-subtext {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue