diff --git a/src/main.ts b/src/main.ts index 6c24ad8..c35fcfd 100644 --- a/src/main.ts +++ b/src/main.ts @@ -24,6 +24,7 @@ import { MarkpilotSettings, MarkpilotSettingTab, } from './settings'; +import { SettingsMigrationsRunner } from './settings/runner'; export default class Markpilot extends Plugin { settings: MarkpilotSettings; @@ -35,6 +36,8 @@ export default class Markpilot extends Plugin { async onload() { await this.loadSettings(); + const runner = new SettingsMigrationsRunner(this); + await runner.apply(); this.addSettingTab(new MarkpilotSettingTab(this.app, this)); const { settings } = this; diff --git a/src/settings/index.ts b/src/settings/index.ts new file mode 100644 index 0000000..123a6bc --- /dev/null +++ b/src/settings/index.ts @@ -0,0 +1 @@ +export type SettingsMigrator = (settings: object) => object; diff --git a/src/settings/migrators/1.1.0-1.2.0.ts b/src/settings/migrators/1.1.0-1.2.0.ts new file mode 100644 index 0000000..c99e949 --- /dev/null +++ b/src/settings/migrators/1.1.0-1.2.0.ts @@ -0,0 +1,33 @@ +import { SettingsMigrator } from '..'; +import { MarkpilotSettings1_1_0 } from '../versions/1.1.0'; +import { MarkpilotSettings1_2_0 } from '../versions/1.2.0'; + +export const migrateVersion1_1_0_toVersion1_2_0: SettingsMigrator = ( + settings: MarkpilotSettings1_1_0, +) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const newSettings: MarkpilotSettings1_2_0 = structuredClone(settings) as any; + newSettings.providers = { + openai: { + apiKey: settings.apiKey, + }, + openrouter: { + apiKey: undefined, + }, + ollama: { + apiUrl: undefined, + }, + }; + newSettings.completions.provider = 'openai'; + newSettings.completions.ignoredFiles = []; + newSettings.completions.ignoredTags = []; + newSettings.chat.provider = 'openai'; + // Update if default models are still selected. + if (settings.completions.model === 'gpt-3.5-turbo-instruct') { + newSettings.completions.model = 'gpt-3.5-turbo'; + } + if (settings.chat.model === 'gpt-3.5-turbo-0125') { + newSettings.chat.model = 'gpt-3.5-turbo'; + } + return newSettings; +}; diff --git a/src/settings/runner.ts b/src/settings/runner.ts new file mode 100644 index 0000000..2c2b8ab --- /dev/null +++ b/src/settings/runner.ts @@ -0,0 +1,29 @@ +import { SettingsMigrator } from '.'; +import Markpilot from '../main'; +import { migrateVersion1_1_0_toVersion1_2_0 } from './migrators/1.1.0-1.2.0'; + +export class SettingsMigrationsRunner { + migrators: Record = { + '1.1.0': migrateVersion1_1_0_toVersion1_2_0, + }; + + constructor(private plugin: Markpilot) {} + + async apply() { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let settings = this.plugin.settings as any; + + while (true) { + // Settings versions and migrations were introduced from version 1.1.0. + const version = settings.version ?? '1.1.0'; + const migrator = this.migrators[version]; + if (migrator === undefined) { + break; + } + settings = migrator(settings); + } + + this.plugin.settings = settings; + await this.plugin.saveSettings(); + } +} diff --git a/src/settings/versions/1.1.0/index.ts b/src/settings/versions/1.1.0/index.ts new file mode 100644 index 0000000..7698133 --- /dev/null +++ b/src/settings/versions/1.1.0/index.ts @@ -0,0 +1,30 @@ +import { ChatCompletionsModel, ChatHistory, CompletionsModel } from './openai'; + +export interface MarkpilotSettings1_1_0 { + apiKey: string | undefined; + completions: { + enabled: boolean; + model: CompletionsModel; + maxTokens: number; + temperature: number; + waitTime: number; + windowSize: number; + acceptKey: string; + rejectKey: string; + }; + chat: { + enabled: boolean; + model: ChatCompletionsModel; + maxTokens: number; + temperature: number; + history: ChatHistory; + }; + cache: { + enabled: boolean; + }; + usage: { + dailyCosts: Record; // e.g. '2021-09-01' to 10.0 (USD) + monthlyCosts: Record; // e.g. '2021-09' to 100.0 (USD) + monthlyLimit: number; + }; +} diff --git a/src/settings/versions/1.1.0/openai.ts b/src/settings/versions/1.1.0/openai.ts new file mode 100644 index 0000000..8484fa5 --- /dev/null +++ b/src/settings/versions/1.1.0/openai.ts @@ -0,0 +1,95 @@ +export const COMPLETIONS_MODELS = [ + 'gpt-3.5-turbo-instruct', + 'davinci-002', + 'babbage-002', +] as const; + +export const CHAT_COMPLETIONS_MODELS = [ + 'gpt-4-0125-preview', + 'gpt-4-turbo-preview', + 'gpt-4-1106-preview', + 'gpt-4-vision-preview', + 'gpt-4', + 'gpt-4-0314', + 'gpt-4-0613', + 'gpt-4-32k', + 'gpt-4-32k-0314', + 'gpt-4-32k-0613', + 'gpt-3.5-turbo', + 'gpt-3.5-turbo-16k', + 'gpt-3.5-turbo-0301', + 'gpt-3.5-turbo-0613', + 'gpt-3.5-turbo-1106', + 'gpt-3.5-turbo-0125', + 'gpt-3.5-turbo-16k-0613', +] as const; + +export const MODEL_INPUT_COSTS: Record< + | (typeof COMPLETIONS_MODELS)[number] + | (typeof CHAT_COMPLETIONS_MODELS)[number], + number +> = { + 'gpt-3.5-turbo-instruct': 1.5, + 'davinci-002': 12.0, + 'babbage-002': 1.6, + 'gpt-4-0125-preview': 10.0, + 'gpt-4-turbo-preview': 10.0, + 'gpt-4-1106-preview': 10.0, + 'gpt-4-vision-preview': 10.0, + 'gpt-4': 30.0, + 'gpt-4-0314': 30.0, + 'gpt-4-0613': 30.0, + 'gpt-4-32k': 60.0, + 'gpt-4-32k-0314': 60.0, + 'gpt-4-32k-0613': 60.0, + 'gpt-3.5-turbo': 0.5, + 'gpt-3.5-turbo-16k': 0.5, + 'gpt-3.5-turbo-0301': 0.5, + 'gpt-3.5-turbo-0613': 0.5, + 'gpt-3.5-turbo-1106': 0.5, + 'gpt-3.5-turbo-0125': 0.5, + 'gpt-3.5-turbo-16k-0613': 0.5, +} as const; + +export const MODEL_OUTPUT_COSTS: Record< + | (typeof COMPLETIONS_MODELS)[number] + | (typeof CHAT_COMPLETIONS_MODELS)[number], + number +> = { + 'gpt-3.5-turbo-instruct': 2.0, + 'davinci-002': 12.0, + 'babbage-002': 1.6, + 'gpt-4-0125-preview': 30, + 'gpt-4-turbo-preview': 30, + 'gpt-4-1106-preview': 30, + 'gpt-4-vision-preview': 30, + 'gpt-4': 60, + 'gpt-4-0314': 60, + 'gpt-4-0613': 60, + 'gpt-4-32k': 120, + 'gpt-4-32k-0314': 120, + 'gpt-4-32k-0613': 120, + 'gpt-3.5-turbo': 1.5, + 'gpt-3.5-turbo-16k': 1.5, + 'gpt-3.5-turbo-0301': 1.5, + 'gpt-3.5-turbo-0613': 1.5, + 'gpt-3.5-turbo-1106': 1.5, + 'gpt-3.5-turbo-0125': 1.5, + 'gpt-3.5-turbo-16k-0613': 1.5, +}; + +export type CompletionsModel = (typeof COMPLETIONS_MODELS)[number]; + +export type ChatCompletionsModel = (typeof CHAT_COMPLETIONS_MODELS)[number]; + +export type ChatRole = 'system' | 'assistant' | 'user'; + +export interface ChatMessage { + role: ChatRole; + content: string; +} + +export interface ChatHistory { + messages: ChatMessage[]; + response: string; +} diff --git a/src/settings/versions/1.2.0/index.ts b/src/settings/versions/1.2.0/index.ts new file mode 100644 index 0000000..5cd7133 --- /dev/null +++ b/src/settings/versions/1.2.0/index.ts @@ -0,0 +1,55 @@ +import { MarkpilotSettings } from 'src/settings'; +import { Equal, Expect } from 'src/utils'; +import { Model } from './models'; +import { Provider } from './provider'; +import { ChatHistory } from './types'; + +export interface MarkpilotSettings1_2_0 { + version: string; + providers: { + openai: { + apiKey: string | undefined; + }; + openrouter: { + apiKey: string | undefined; + }; + ollama: { + apiUrl: string | undefined; + }; + }; + completions: { + enabled: boolean; + provider: Provider; + model: Model; + maxTokens: number; + temperature: number; + waitTime: number; + windowSize: number; + acceptKey: string; + rejectKey: string; + ignoredFiles: string[]; + ignoredTags: string[]; + }; + chat: { + enabled: boolean; + provider: Provider; + model: Model; + maxTokens: number; + temperature: number; + history: ChatHistory; + }; + cache: { + enabled: boolean; + }; + usage: { + dailyCosts: Record; // e.g. '2021-09-01' to 10.0 (USD) + monthlyCosts: Record; // e.g. '2021-09' to 100.0 (USD) + monthlyLimit: number; + }; +} + +// Check the settings type in this version matches the current settings type. +// eslint-disable-next-line @typescript-eslint/no-unused-vars +type AssertEqualCurrentSettings = Expect< + Equal +>; diff --git a/src/settings/versions/1.2.0/models.ts b/src/settings/versions/1.2.0/models.ts new file mode 100644 index 0000000..b205f06 --- /dev/null +++ b/src/settings/versions/1.2.0/models.ts @@ -0,0 +1,60 @@ +import { Provider } from './provider'; + +export type OpenAIModel = (typeof OPENAI_MODELS)[number]; + +export type OpenRouterModel = (typeof OPENROUTER_MODELS)[number]; + +export type OllamaModel = (typeof OLLAMA_MODELS)[number]; + +export type Model = OpenAIModel | OpenRouterModel | OllamaModel; + +export const OPENAI_MODELS = [ + 'gpt-3.5-turbo-instruct', + 'davinci-002', + 'babbage-002', + 'gpt-4-0125-preview', + 'gpt-4-turbo-preview', + 'gpt-4-1106-preview', + 'gpt-4-vision-preview', + 'gpt-4', + 'gpt-4-0314', + 'gpt-4-0613', + 'gpt-4-32k', + 'gpt-4-32k-0314', + 'gpt-4-32k-0613', + 'gpt-3.5-turbo', + 'gpt-3.5-turbo-16k', + 'gpt-3.5-turbo-0301', + 'gpt-3.5-turbo-0613', + 'gpt-3.5-turbo-1106', + 'gpt-3.5-turbo-0125', + 'gpt-3.5-turbo-16k-0613', +] as const; + +// TODO: +// This is a placeholder. +export const OPENROUTER_MODELS = [ + 'openai/gpt-3.5-turbo', + 'openai/gpt-4-turbo', +] as const; + +// TODO: +// This is a placeholder. +export const OLLAMA_MODELS = [ + 'llama2', + 'llama3', + 'codellama', + 'phind-codellama', +] as const; + +export const MODELS = { + openai: OPENAI_MODELS, + openrouter: OPENROUTER_MODELS, + ollama: OLLAMA_MODELS, +}; + +export const DEFAULT_MODELS: Record = { + openai: 'gpt-3.5-turbo', + openrouter: 'openai/gpt-3.5-turbo', + ollama: 'llama2', +}; diff --git a/src/settings/versions/1.2.0/provider.ts b/src/settings/versions/1.2.0/provider.ts new file mode 100644 index 0000000..9b8fcb9 --- /dev/null +++ b/src/settings/versions/1.2.0/provider.ts @@ -0,0 +1,13 @@ +export type OnlineProvider = (typeof ONLINE_PROVIDERS)[number]; + +export type OfflineProvider = (typeof OFFLINE_PROVIDERS)[number]; + +export type Provider = OnlineProvider | OfflineProvider; + +export const ONLINE_PROVIDERS = ['openai', 'openrouter'] as const; + +export const OFFLINE_PROVIDERS = ['ollama'] as const; + +export const PROVIDERS = [...ONLINE_PROVIDERS, ...OFFLINE_PROVIDERS] as const; + +export const DEFAULT_PROVIDER = 'openai' as Provider; diff --git a/src/settings/versions/1.2.0/types.ts b/src/settings/versions/1.2.0/types.ts new file mode 100644 index 0000000..b7f4280 --- /dev/null +++ b/src/settings/versions/1.2.0/types.ts @@ -0,0 +1,11 @@ +export type ChatRole = 'system' | 'assistant' | 'user'; + +export interface ChatMessage { + role: ChatRole; + content: string; +} + +export interface ChatHistory { + messages: ChatMessage[]; + response: string; +} diff --git a/src/utils.ts b/src/utils.ts index 74722de..e7140ab 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -69,3 +69,10 @@ export function getDaysInCurrentMonth(): Date[] { } return dates; } + +// Utility types used for settings migration. +export type Expect = T; +export type Equal = + (() => T extends X ? 1 : 2) extends () => T extends Y ? 1 : 2 + ? true + : false;