mirror of
https://github.com/andy-stack/vaultkeeper-ai.git
synced 2026-07-22 16:30:27 +00:00
refactor: replace API provider dropdown with model selection dropdown
Replace simple API provider selection with comprehensive model dropdown organized by provider (Claude, OpenAI, Gemini). Update settings schema from `apiProvider` to `model`, add AIProvider.fromModel() helper, simplify provider URLs, and fix indentation inconsistencies throughout codebase.
This commit is contained in:
parent
9f36dac64c
commit
ba77ebc81b
13 changed files with 199 additions and 63 deletions
|
|
@ -1,4 +1,5 @@
|
|||
import { AIProvider } from "Enums/ApiProvider";
|
||||
import { AIProviderModel } from "Enums/ApiProvider";
|
||||
import { Copy } from "Enums/Copy";
|
||||
import { Path } from "Enums/Path";
|
||||
import { Selector } from "Enums/Selector";
|
||||
import type AIAgentPlugin from "main";
|
||||
|
|
@ -18,21 +19,100 @@ export class AIAgentSettingTab extends PluginSettingTab {
|
|||
|
||||
containerEl.empty();
|
||||
|
||||
/* API Provider Setting */
|
||||
/* Model Selection Setting */
|
||||
new Setting(containerEl)
|
||||
.setName("API Provider")
|
||||
.setDesc("Select the API provider to use.")
|
||||
.addDropdown((dropdown) =>
|
||||
dropdown
|
||||
.addOption(AIProvider.Claude, AIProvider.Claude)
|
||||
.addOption(AIProvider.Gemini, AIProvider.Gemini)
|
||||
.addOption(AIProvider.OpenAI, AIProvider.OpenAI)
|
||||
.setValue(this.plugin.settings.apiProvider)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.apiProvider = value;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
.setName("Model")
|
||||
.setDesc("Select the AI model to use.")
|
||||
.addDropdown((dropdown) => {
|
||||
const select = dropdown.selectEl;
|
||||
|
||||
// Claude models group
|
||||
const claudeGroup = select.createEl("optgroup", { attr: { label: "Claude" } });
|
||||
claudeGroup.createEl("option", {
|
||||
value: AIProviderModel.ClaudeSonnet_4_5,
|
||||
text: Copy.ClaudeSonnet_4_5
|
||||
});
|
||||
claudeGroup.createEl("option", {
|
||||
value: AIProviderModel.ClaudeSonnet_4,
|
||||
text: Copy.ClaudeSonnet_4
|
||||
});
|
||||
claudeGroup.createEl("option", {
|
||||
value: AIProviderModel.ClaudeSonnet_3_7,
|
||||
text: Copy.ClaudeSonnet_3_7
|
||||
});
|
||||
claudeGroup.createEl("option", {
|
||||
value: AIProviderModel.ClaudeOpus_4_1,
|
||||
text: Copy.ClaudeOpus_4_1
|
||||
});
|
||||
claudeGroup.createEl("option", {
|
||||
value: AIProviderModel.ClaudeOpus_4,
|
||||
text: Copy.ClaudeOpus_4
|
||||
});
|
||||
claudeGroup.createEl("option", {
|
||||
value: AIProviderModel.ClaudeHaiku_4_5,
|
||||
text: Copy.ClaudeHaiku_4_5
|
||||
});
|
||||
|
||||
// OpenAI models group
|
||||
const openaiGroup = select.createEl("optgroup", { attr: { label: "OpenAI" } });
|
||||
openaiGroup.createEl("option", {
|
||||
value: AIProviderModel.GPT_5,
|
||||
text: Copy.GPT_5
|
||||
});
|
||||
openaiGroup.createEl("option", {
|
||||
value: AIProviderModel.GPT_5_Mini,
|
||||
text: Copy.GPT_5_Mini
|
||||
});
|
||||
openaiGroup.createEl("option", {
|
||||
value: AIProviderModel.GPT_5_Nano,
|
||||
text: Copy.GPT_5_Nano
|
||||
});
|
||||
openaiGroup.createEl("option", {
|
||||
value: AIProviderModel.GPT_5_Pro,
|
||||
text: Copy.GPT_5_Pro
|
||||
});
|
||||
openaiGroup.createEl("option", {
|
||||
value: AIProviderModel.GPT_4o,
|
||||
text: Copy.GPT_4o
|
||||
});
|
||||
openaiGroup.createEl("option", {
|
||||
value: AIProviderModel.GPT_4o_Mini,
|
||||
text: Copy.GPT_4o_Mini
|
||||
});
|
||||
openaiGroup.createEl("option", {
|
||||
value: AIProviderModel.GPT_4_1,
|
||||
text: Copy.GPT_4_1
|
||||
});
|
||||
openaiGroup.createEl("option", {
|
||||
value: AIProviderModel.GPT_4_1_Mini,
|
||||
text: Copy.GPT_4_1_Mini
|
||||
});
|
||||
openaiGroup.createEl("option", {
|
||||
value: AIProviderModel.GPT_4_1_Nano,
|
||||
text: Copy.GPT_4_1_Nano
|
||||
});
|
||||
|
||||
// Gemini models group
|
||||
const geminiGroup = select.createEl("optgroup", { attr: { label: "Gemini" } });
|
||||
geminiGroup.createEl("option", {
|
||||
value: AIProviderModel.GeminiFlash_2_5_Lite,
|
||||
text: Copy.GeminiFlash_2_5_Lite
|
||||
});
|
||||
geminiGroup.createEl("option", {
|
||||
value: AIProviderModel.GeminiFlash_2_5,
|
||||
text: Copy.GeminiFlash_2_5
|
||||
});
|
||||
geminiGroup.createEl("option", {
|
||||
value: AIProviderModel.GeminiPro_2_5,
|
||||
text: Copy.GeminiPro_2_5
|
||||
});
|
||||
|
||||
dropdown.setValue(this.plugin.settings.model);
|
||||
dropdown.onChange(async (value) => {
|
||||
this.plugin.settings.model = value;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
|
||||
/* API Key Setting */
|
||||
let apiKeyInputEl: HTMLInputElement;
|
||||
|
|
@ -57,11 +137,11 @@ export class AIAgentSettingTab extends PluginSettingTab {
|
|||
if (apiKeyInputEl.type === "password") {
|
||||
apiKeyInputEl.type = "text";
|
||||
setIcon(button.extraSettingsEl, "eye-off");
|
||||
setTooltip(button.extraSettingsEl, "Hide API Key");
|
||||
setTooltip(button.extraSettingsEl, "Hide API Key");
|
||||
} else {
|
||||
apiKeyInputEl.type = "password";
|
||||
setIcon(button.extraSettingsEl, "eye");
|
||||
setTooltip(button.extraSettingsEl, "Show API Key");
|
||||
setTooltip(button.extraSettingsEl, "Show API Key");
|
||||
}
|
||||
});
|
||||
setIcon(button.extraSettingsEl, "eye");
|
||||
|
|
@ -85,13 +165,13 @@ export class AIAgentSettingTab extends PluginSettingTab {
|
|||
|
||||
private highlightApiKey(): void {
|
||||
if (this.apiKeySetting) {
|
||||
if (this.plugin.settings.apiKey.trim() === "") {
|
||||
this.apiKeySetting.settingEl.removeClass(Selector.ApiKeySettingOk);
|
||||
this.apiKeySetting.settingEl.addClass(Selector.ApiKeySettingError);
|
||||
} else {
|
||||
this.apiKeySetting.settingEl.removeClass(Selector.ApiKeySettingError);
|
||||
this.apiKeySetting.settingEl.addClass(Selector.ApiKeySettingOk);
|
||||
}
|
||||
if (this.plugin.settings.apiKey.trim() === "") {
|
||||
this.apiKeySetting.settingEl.removeClass(Selector.ApiKeySettingOk);
|
||||
this.apiKeySetting.settingEl.addClass(Selector.ApiKeySettingError);
|
||||
} else {
|
||||
this.apiKeySetting.settingEl.removeClass(Selector.ApiKeySettingError);
|
||||
this.apiKeySetting.settingEl.addClass(Selector.ApiKeySettingOk);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,7 +4,7 @@ import type { IAIClass } from "AIClasses/IAIClass";
|
|||
import type { IPrompt } from "AIClasses/IPrompt";
|
||||
import { StreamingService, type StreamChunk } from "Services/StreamingService";
|
||||
import type { Conversation } from "Conversations/Conversation";
|
||||
import { AIProviderURL, AIProviderModel } from "Enums/ApiProvider";
|
||||
import { AIProviderURL } from "Enums/ApiProvider";
|
||||
import { AIFunctionCall } from "AIClasses/AIFunctionCall";
|
||||
import type { IAIFunctionDefinition } from "AIClasses/FunctionDefinitions/IAIFunctionDefinition";
|
||||
import type AIAgentPlugin from "main";
|
||||
|
|
@ -49,7 +49,7 @@ export class Claude implements IAIClass {
|
|||
);
|
||||
|
||||
const requestBody = {
|
||||
model: AIProviderModel.Claude,
|
||||
model: this.plugin.settings.model,
|
||||
max_tokens: 16384,
|
||||
system: systemPrompt,
|
||||
messages: messages,
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ export class ClaudeConversationNamingService implements IConversationNamingServi
|
|||
}]
|
||||
};
|
||||
|
||||
const response = await fetch(AIProviderURL.ClaudeNamer, {
|
||||
const response = await fetch(AIProviderURL.Claude, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'x-api-key': this.apiKey,
|
||||
|
|
|
|||
|
|
@ -3,18 +3,20 @@ import Anthropic from '@anthropic-ai/sdk'
|
|||
import type AIAgentPlugin from "main";
|
||||
import { Resolve } from "Services/DependencyService";
|
||||
import { Services } from "Services/Services";
|
||||
import { AIProviderModel } from "Enums/ApiProvider";
|
||||
import { Role } from "Enums/Role";
|
||||
|
||||
export class ClaudeTokenService implements ITokenService {
|
||||
|
||||
private ai: Anthropic;
|
||||
private model: string;
|
||||
|
||||
public constructor() {
|
||||
const plugin: AIAgentPlugin = Resolve<AIAgentPlugin>(Services.AIAgentPlugin);
|
||||
this.ai = new Anthropic({
|
||||
apiKey: Resolve<AIAgentPlugin>(Services.AIAgentPlugin).settings.apiKey,
|
||||
apiKey: plugin.settings.apiKey,
|
||||
dangerouslyAllowBrowser: true
|
||||
})
|
||||
});
|
||||
this.model = plugin.settings.model;
|
||||
}
|
||||
|
||||
public async countTokens(input: string): Promise<number> {
|
||||
|
|
@ -24,7 +26,7 @@ export class ClaudeTokenService implements ITokenService {
|
|||
|
||||
// to maintain the convenience of the interface we just submit the entire input as one message
|
||||
const result = await this.ai.messages.countTokens({
|
||||
model: AIProviderModel.Claude,
|
||||
model: this.model,
|
||||
messages: [
|
||||
{ role: Role.User, content: input }
|
||||
]
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ export class Gemini implements IAIClass {
|
|||
};
|
||||
|
||||
yield* this.streamingService.streamRequest(
|
||||
AIProviderURL.Gemini.replace("API_KEY", this.apiKey),
|
||||
`${AIProviderURL.Gemini}/${this.plugin.settings.model}:streamGenerateContent?key=${this.apiKey}&alt=sse`,
|
||||
requestBody,
|
||||
this.parseStreamChunk.bind(this),
|
||||
abortSignal
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { Resolve } from "Services/DependencyService";
|
|||
import { Services } from "Services/Services";
|
||||
import type { IConversationNamingService } from "AIClasses/IConversationNamingService";
|
||||
import type AIAgentPlugin from "main";
|
||||
import { AIProviderURL } from "Enums/ApiProvider";
|
||||
import { AIProviderURL, AIProviderModel } from "Enums/ApiProvider";
|
||||
import { Role } from "Enums/Role";
|
||||
import { NamePrompt } from "AIClasses/NamePrompt";
|
||||
|
||||
|
|
@ -26,7 +26,7 @@ export class GeminiConversationNamingService implements IConversationNamingServi
|
|||
}]
|
||||
};
|
||||
|
||||
const response = await fetch(AIProviderURL.GeminiNamer.replace("API_KEY", this.apiKey), {
|
||||
const response = await fetch(`${AIProviderURL.Gemini}/${AIProviderModel.GeminiNamer}:generateContent?key=${this.apiKey}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
|
|
|||
|
|
@ -3,16 +3,18 @@ import { CountTokensResponse, GoogleGenAI } from '@google/genai'
|
|||
import type AIAgentPlugin from "main";
|
||||
import { Resolve } from "Services/DependencyService";
|
||||
import { Services } from "Services/Services";
|
||||
import { AIProviderModel } from "Enums/ApiProvider";
|
||||
|
||||
export class GeminiTokenService implements ITokenService {
|
||||
|
||||
private readonly ai: GoogleGenAI;
|
||||
private model: string;
|
||||
|
||||
public constructor() {
|
||||
this.ai = new GoogleGenAI({
|
||||
apiKey: Resolve<AIAgentPlugin>(Services.AIAgentPlugin).settings.apiKey
|
||||
const plugin: AIAgentPlugin = Resolve<AIAgentPlugin>(Services.AIAgentPlugin);
|
||||
this.ai = new GoogleGenAI({
|
||||
apiKey: plugin.settings.apiKey
|
||||
});
|
||||
this.model = plugin.settings.model;
|
||||
}
|
||||
|
||||
public async countTokens(input: string): Promise<number> {
|
||||
|
|
@ -21,7 +23,7 @@ export class GeminiTokenService implements ITokenService {
|
|||
}
|
||||
|
||||
const result: CountTokensResponse = await this.ai.models.countTokens({
|
||||
model: AIProviderModel.Gemini,
|
||||
model: this.model,
|
||||
contents: input
|
||||
});
|
||||
return result.totalTokens ?? -1;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import type { IAIClass } from "AIClasses/IAIClass";
|
|||
import type { IPrompt } from "AIClasses/IPrompt";
|
||||
import { StreamingService, type StreamChunk } from "Services/StreamingService";
|
||||
import type { Conversation } from "Conversations/Conversation";
|
||||
import { AIProviderURL, AIProviderModel } from "Enums/ApiProvider";
|
||||
import { AIProviderURL } from "Enums/ApiProvider";
|
||||
import { AIFunctionCall } from "AIClasses/AIFunctionCall";
|
||||
import type { IAIFunctionDefinition } from "AIClasses/FunctionDefinitions/IAIFunctionDefinition";
|
||||
import type AIAgentPlugin from "main";
|
||||
|
|
@ -130,7 +130,7 @@ export class OpenAI implements IAIClass {
|
|||
);
|
||||
|
||||
const requestBody = {
|
||||
model: AIProviderModel.OpenAI,
|
||||
model: this.plugin.settings.model,
|
||||
messages: messages,
|
||||
tools: tools,
|
||||
stream: true
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ export class OpenAIConversationNamingService implements IConversationNamingServi
|
|||
]
|
||||
};
|
||||
|
||||
const response = await fetch(AIProviderURL.OpenAINamer, {
|
||||
const response = await fetch(AIProviderURL.OpenAI, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${this.apiKey}`,
|
||||
|
|
|
|||
|
|
@ -2,25 +2,55 @@ export enum AIProvider {
|
|||
Claude = "Claude",
|
||||
Gemini = "Gemini",
|
||||
OpenAI = "OpenAI"
|
||||
};
|
||||
}
|
||||
|
||||
export namespace AIProvider {
|
||||
export function fromModel(model: string): AIProvider {
|
||||
if (model.startsWith("claude-")) {
|
||||
return AIProvider.Claude;
|
||||
} else if (model.startsWith("gemini-")) {
|
||||
return AIProvider.Gemini;
|
||||
} else if (model.startsWith("gpt-")) {
|
||||
return AIProvider.OpenAI;
|
||||
} else {
|
||||
throw new Error("Invalid Model Selection!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export enum AIProviderModel {
|
||||
Claude = "claude-sonnet-4-5-20250929",
|
||||
Gemini = "gemini-2.5-flash",
|
||||
OpenAI = "gpt-4o",
|
||||
// Claude models
|
||||
ClaudeSonnet_4_5 = "claude-sonnet-4-5-20250929",
|
||||
ClaudeSonnet_4 = "claude-sonnet-4-20250514",
|
||||
ClaudeSonnet_3_7 = "claude-3-7-sonnet-20250219",
|
||||
ClaudeOpus_4_1 = "claude-opus-4-1-20250805",
|
||||
ClaudeOpus_4 = "claude-opus-4-20250514",
|
||||
ClaudeHaiku_4_5 = "claude-haiku-4-5-20251001",
|
||||
|
||||
// Gemini models
|
||||
GeminiFlash_2_5_Lite = "gemini-2.5-flash-lite",
|
||||
GeminiFlash_2_5 = "gemini-2.5-flash",
|
||||
GeminiPro_2_5 = "gemini-2.5-pro",
|
||||
|
||||
// OpenAI models
|
||||
GPT_5 = "gpt-5",
|
||||
GPT_5_Mini = "gpt-5-mini",
|
||||
GPT_5_Nano = "gpt-5-nano",
|
||||
GPT_5_Pro = "gpt-5-pro",
|
||||
GPT_4o = "gpt-4o",
|
||||
GPT_4o_Mini = "gpt-4o-mini",
|
||||
GPT_4_1 = "gpt-4.1",
|
||||
GPT_4_1_Mini = "gpt-4.1-mini",
|
||||
GPT_4_1_Nano = "gpt-4.1-nano",
|
||||
|
||||
// Conversation naming models
|
||||
ClaudeNamer = "claude-haiku-4-5-20251001",
|
||||
GeminiNamer = "gemini-2.5-flash",
|
||||
GeminiNamer = "gemini-2.5-flash-lite",
|
||||
OpenAINamer = "gpt-4o-mini",
|
||||
}
|
||||
|
||||
export enum AIProviderURL {
|
||||
Claude = "https://api.anthropic.com/v1/messages",
|
||||
ClaudeNamer = "https://api.anthropic.com/v1/messages",
|
||||
|
||||
Gemini = `https://generativelanguage.googleapis.com/v1beta/models/${AIProviderModel.GeminiNamer}:streamGenerateContent?key=API_KEY&alt=sse`,
|
||||
GeminiNamer = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=API_KEY",
|
||||
|
||||
OpenAI = "https://api.openai.com/v1/chat/completions",
|
||||
OpenAINamer = "https://api.openai.com/v1/chat/completions"
|
||||
Gemini = "https://generativelanguage.googleapis.com/v1beta/models",
|
||||
OpenAI = "https://api.openai.com/v1/chat/completions"
|
||||
}
|
||||
|
|
@ -1,3 +1,25 @@
|
|||
export enum Copy {
|
||||
ApiRequestAborted = "Request has been cancelled"
|
||||
ApiRequestAborted = "Request has been cancelled",
|
||||
|
||||
// Model display names
|
||||
ClaudeSonnet_4_5 = "Claude Sonnet 4.5",
|
||||
ClaudeSonnet_4 = "Claude Sonnet 4",
|
||||
ClaudeSonnet_3_7 = "Claude Sonnet 3.7",
|
||||
ClaudeOpus_4_1 = "Claude Opus 4.1",
|
||||
ClaudeOpus_4 = "Claude Opus 4",
|
||||
ClaudeHaiku_4_5 = "Claude Haiku 4.5",
|
||||
|
||||
GeminiFlash_2_5_Lite = "Gemini 2.5 Flash Lite",
|
||||
GeminiFlash_2_5 = "Gemini 2.5 Flash",
|
||||
GeminiPro_2_5 = "Gemini 2.5 Pro",
|
||||
|
||||
GPT_5 = "GPT-5",
|
||||
GPT_5_Mini = "GPT-5 Mini",
|
||||
GPT_5_Nano = "GPT-5 Nano",
|
||||
GPT_5_Pro = "GPT-5 Pro",
|
||||
GPT_4o = "GPT-4o",
|
||||
GPT_4o_Mini = "GPT-4o Mini",
|
||||
GPT_4_1 = "GPT-4.1",
|
||||
GPT_4_1_Mini = "GPT-4.1 Mini",
|
||||
GPT_4_1_Nano = "GPT-4.1 Nano",
|
||||
}
|
||||
|
|
@ -52,24 +52,24 @@ export function RegisterDependencies(plugin: AIAgentPlugin) {
|
|||
}
|
||||
|
||||
export function RegisterAiProvider(plugin: AIAgentPlugin) {
|
||||
if (plugin.settings.apiProvider == AIProvider.Claude) {
|
||||
const provider = AIProvider.fromModel(plugin.settings.model);
|
||||
|
||||
if (provider == AIProvider.Claude) {
|
||||
RegisterSingleton<IAIClass>(Services.IAIClass, new Claude());
|
||||
RegisterSingleton<ITokenService>(Services.ITokenService, new ClaudeTokenService());
|
||||
RegisterSingleton<IConversationNamingService>(Services.IConversationNamingService, new ClaudeConversationNamingService());
|
||||
}
|
||||
else if (plugin.settings.apiProvider == AIProvider.Gemini) {
|
||||
else if (provider == AIProvider.Gemini) {
|
||||
RegisterSingleton<IAIClass>(Services.IAIClass, new Gemini());
|
||||
RegisterSingleton<ITokenService>(Services.ITokenService, new GeminiTokenService());
|
||||
RegisterSingleton<IConversationNamingService>(Services.IConversationNamingService, new GeminiConversationNamingService());
|
||||
}
|
||||
else if (plugin.settings.apiProvider == AIProvider.OpenAI) {
|
||||
else if (provider == AIProvider.OpenAI) {
|
||||
RegisterSingleton<IAIClass>(Services.IAIClass, new OpenAI());
|
||||
RegisterSingleton<ITokenService>(Services.ITokenService, new OpenAITokenService());
|
||||
RegisterSingleton<IConversationNamingService>(Services.IConversationNamingService, new OpenAIConversationNamingService());
|
||||
}
|
||||
else { // should be impossible to land here
|
||||
throw new Error("Invalid Provider Selection!");
|
||||
}
|
||||
|
||||
Resolve<ChatService>(Services.ChatService).resolveAIProvider();
|
||||
Resolve<ConversationNamingService>(Services.ConversationNamingService).resolveNamingProvider();
|
||||
}
|
||||
|
|
|
|||
6
main.ts
6
main.ts
|
|
@ -1,5 +1,5 @@
|
|||
import { WorkspaceLeaf, Plugin } from 'obsidian';
|
||||
import { AIProvider } from './Enums/ApiProvider';
|
||||
import { AIProviderModel } from './Enums/ApiProvider';
|
||||
import { MainView, VIEW_TYPE_MAIN } from 'Views/MainView';
|
||||
import { RegisterAiProvider, RegisterDependencies } from 'Services/ServiceRegistration';
|
||||
import { AIAgentSettingTab } from 'AIAgentSettingTab';
|
||||
|
|
@ -8,13 +8,13 @@ import type { StatusBarService } from 'Services/StatusBarService';
|
|||
import { Resolve } from 'Services/DependencyService';
|
||||
|
||||
interface IAIAgentSettings {
|
||||
apiProvider: string;
|
||||
model: string;
|
||||
apiKey: string;
|
||||
exclusions: string[];
|
||||
}
|
||||
|
||||
const DEFAULT_SETTINGS: IAIAgentSettings = {
|
||||
apiProvider: AIProvider.Gemini,
|
||||
model: AIProviderModel.ClaudeSonnet_4_5,
|
||||
apiKey: "",
|
||||
exclusions: []
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue