Auto tagging obsidian notes w/ AI
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: add support for multiple AI providers

- Add support for OpenAI, Mistral AI, Google (Gemini), and custom OpenAI-compatible endpoints
- Update settings UI to show provider-specific model options
- Implement provider-specific API calls in generateTags function
- Add validation for custom endpoints
- Show model-specific information in confirmation dialogs

🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>

+302 -41
+302 -41
main.ts
··· 10 10 requestUrl, 11 11 } from "obsidian"; 12 12 13 + type AIProvider = 'anthropic' | 'openai' | 'mistral' | 'google' | 'custom'; 14 + 13 15 interface AITaggerSettings { 16 + provider: AIProvider; 14 17 apiKey: string; 15 18 modelName: string; 16 19 maxTags: number; 17 20 promptOption: string; 18 21 promptTemplate: string; 22 + customEndpoint: string; 19 23 } 20 24 21 25 // Define prompt templates ··· 27 31 custom: "", 28 32 }; 29 33 34 + // Define model configurations for each provider 35 + const MODEL_CONFIGS = { 36 + anthropic: { 37 + apiUrl: "https://api.anthropic.com/v1/messages", 38 + models: [ 39 + { id: "claude-3-5-sonnet-20240620", name: "Claude 3.5 Sonnet" }, 40 + { id: "claude-3-opus-20240229", name: "Claude 3 Opus" }, 41 + { id: "claude-3-sonnet-20240229", name: "Claude 3 Sonnet" }, 42 + { id: "claude-3-haiku-20240307", name: "Claude 3 Haiku" }, 43 + ], 44 + defaultModel: "claude-3-5-sonnet-20240620", 45 + apiKeyUrl: "https://console.anthropic.com/", 46 + }, 47 + openai: { 48 + apiUrl: "https://api.openai.com/v1/chat/completions", 49 + models: [ 50 + { id: "gpt-4o", name: "GPT-4o" }, 51 + { id: "gpt-4-turbo", name: "GPT-4 Turbo" }, 52 + { id: "gpt-4", name: "GPT-4" }, 53 + { id: "gpt-3.5-turbo", name: "GPT-3.5 Turbo" }, 54 + ], 55 + defaultModel: "gpt-3.5-turbo", 56 + apiKeyUrl: "https://platform.openai.com/api-keys", 57 + }, 58 + mistral: { 59 + apiUrl: "https://api.mistral.ai/v1/chat/completions", 60 + models: [ 61 + { id: "mistral-large-latest", name: "Mistral Large" }, 62 + { id: "mistral-medium-latest", name: "Mistral Medium" }, 63 + { id: "mistral-small-latest", name: "Mistral Small" }, 64 + { id: "open-mistral-7b", name: "Open Mistral 7B" }, 65 + ], 66 + defaultModel: "mistral-small-latest", 67 + apiKeyUrl: "https://console.mistral.ai/api-keys/", 68 + }, 69 + google: { 70 + apiUrl: "https://generativelanguage.googleapis.com/v1beta/models/", 71 + models: [ 72 + { id: "gemini-1.5-pro", name: "Gemini 1.5 Pro" }, 73 + { id: "gemini-1.5-flash", name: "Gemini 1.5 Flash" }, 74 + { id: "gemini-1.0-pro", name: "Gemini 1.0 Pro" }, 75 + ], 76 + defaultModel: "gemini-1.5-flash", 77 + apiKeyUrl: "https://aistudio.google.com/app/apikey", 78 + }, 79 + custom: { 80 + apiUrl: "", 81 + models: [ 82 + { id: "custom-model", name: "Custom Model" }, 83 + ], 84 + defaultModel: "custom-model", 85 + apiKeyUrl: "", 86 + } 87 + }; 88 + 30 89 const DEFAULT_SETTINGS: AITaggerSettings = { 90 + provider: "anthropic", 31 91 apiKey: "", 32 - modelName: "claude-3-5-sonnet-20240620", 92 + modelName: MODEL_CONFIGS.anthropic.defaultModel, 33 93 maxTags: 5, 34 94 promptOption: "standard", 35 95 promptTemplate: PROMPT_TEMPLATES.standard, 96 + customEndpoint: "", 36 97 }; 37 98 38 99 export default class AITaggerPlugin extends Plugin { ··· 46 107 "tag", 47 108 "Auto-tag with AI", 48 109 (evt: MouseEvent) => { 110 + // Check for required settings based on provider 49 111 if (!this.settings.apiKey) { 50 112 new ConfirmModal( 51 113 this.app, ··· 56 118 ).open(); 57 119 return; 58 120 } 121 + 122 + if (this.settings.provider === "custom" && !this.settings.customEndpoint) { 123 + new ConfirmModal( 124 + this.app, 125 + "Custom API endpoint missing", 126 + () => {}, 127 + true, 128 + "Custom API endpoint not configured. Please add your endpoint URL in the plugin settings." 129 + ).open(); 130 + return; 131 + } 132 + 59 133 // Tag the current note directly when clicking the ribbon icon 60 134 this.tagCurrentNote(); 61 135 } ··· 81 155 ).open(); 82 156 return true; 83 157 } 158 + 159 + if (this.settings.provider === "custom" && !this.settings.customEndpoint) { 160 + new ConfirmModal( 161 + this.app, 162 + "Custom API endpoint missing", 163 + () => {}, 164 + true, 165 + "Custom API endpoint not configured. Please add your endpoint URL in the plugin settings." 166 + ).open(); 167 + return true; 168 + } 169 + 84 170 this.tagCurrentNote(); 85 171 } 86 172 return true; ··· 104 190 ).open(); 105 191 return; 106 192 } 193 + 194 + if (this.settings.provider === "custom" && !this.settings.customEndpoint) { 195 + new ConfirmModal( 196 + this.app, 197 + "Custom API endpoint missing", 198 + () => {}, 199 + true, 200 + "Custom API endpoint not configured. Please add your endpoint URL in the plugin settings." 201 + ).open(); 202 + return; 203 + } 107 204 108 205 new ConfirmModal( 109 206 this.app, 110 - "This will tag all notes in your vault. This may take a while and consume API credits. Do you want to continue?", 207 + `This will tag all notes in your vault using the ${MODEL_CONFIGS[this.settings.provider].models.find(m => m.id === this.settings.modelName)?.name || this.settings.modelName} model. This may take a while and consume API credits. Do you want to continue?`, 111 208 () => this.tagAllNotes() 112 209 ).open(); 113 210 }, ··· 133 230 if (!this.settings.apiKey) { 134 231 new Notice( 135 232 "API key not configured. Please add your API key in the plugin settings." 233 + ); 234 + return; 235 + } 236 + 237 + if (this.settings.provider === "custom" && !this.settings.customEndpoint) { 238 + new Notice( 239 + "Custom API endpoint not configured. Please add your endpoint URL in the plugin settings." 136 240 ); 137 241 return; 138 242 } ··· 181 285 ); 182 286 return; 183 287 } 288 + 289 + if (this.settings.provider === "custom" && !this.settings.customEndpoint) { 290 + new Notice( 291 + "Custom API endpoint not configured. Please add your endpoint URL in the plugin settings." 292 + ); 293 + return; 294 + } 184 295 185 296 const files = this.app.vault.getMarkdownFiles(); 186 297 let processed = 0; ··· 221 332 ); 222 333 } 223 334 335 + if (this.settings.provider === "custom" && !this.settings.customEndpoint) { 336 + throw new Error( 337 + "Custom API endpoint not configured. Please add your endpoint URL in the plugin settings." 338 + ); 339 + } 340 + 224 341 // Replace placeholders in the prompt template 225 342 const prompt = this.settings.promptTemplate 226 343 .replace("{maxTags}", this.settings.maxTags.toString()) 227 344 .replace("{content}", content); 228 345 229 346 try { 230 - const response = await requestUrl({ 231 - url: "https://api.anthropic.com/v1/messages", 232 - method: "POST", 233 - headers: { 234 - "Content-Type": "application/json", 235 - "x-api-key": this.settings.apiKey, 236 - "anthropic-version": "2023-06-01", 237 - }, 238 - body: JSON.stringify({ 239 - model: this.settings.modelName, 240 - max_tokens: 1000, 241 - messages: [ 242 - { 243 - role: "user", 244 - content: prompt, 347 + let response; 348 + let tagText: string; 349 + 350 + // Get provider configuration 351 + const providerConfig = MODEL_CONFIGS[this.settings.provider]; 352 + 353 + switch (this.settings.provider) { 354 + case "anthropic": 355 + response = await requestUrl({ 356 + url: providerConfig.apiUrl, 357 + method: "POST", 358 + headers: { 359 + "Content-Type": "application/json", 360 + "x-api-key": this.settings.apiKey, 361 + "anthropic-version": "2023-06-01", 362 + }, 363 + body: JSON.stringify({ 364 + model: this.settings.modelName, 365 + max_tokens: 1000, 366 + messages: [ 367 + { 368 + role: "user", 369 + content: prompt, 370 + }, 371 + ], 372 + }), 373 + }); 374 + 375 + if (response.status !== 200) { 376 + throw new Error( 377 + `API returned status code ${response.status}: ${JSON.stringify( 378 + response.json 379 + )}` 380 + ); 381 + } 382 + 383 + tagText = response.json.content[0].text; 384 + break; 385 + 386 + case "openai": 387 + case "mistral": 388 + case "custom": 389 + // OpenAI and Mistral use the same API format 390 + // For custom endpoints, we assume OpenAI compatible format 391 + const endpoint = this.settings.provider === "custom" 392 + ? this.settings.customEndpoint 393 + : providerConfig.apiUrl; 394 + 395 + response = await requestUrl({ 396 + url: endpoint, 397 + method: "POST", 398 + headers: { 399 + "Content-Type": "application/json", 400 + "Authorization": `Bearer ${this.settings.apiKey}`, 245 401 }, 246 - ], 247 - }), 248 - }); 249 - 250 - if (response.status !== 200) { 251 - throw new Error( 252 - `API returned status code ${response.status}: ${JSON.stringify( 253 - response.json 254 - )}` 255 - ); 402 + body: JSON.stringify({ 403 + model: this.settings.modelName, 404 + messages: [ 405 + { 406 + role: "user", 407 + content: prompt, 408 + }, 409 + ], 410 + max_tokens: 1000, 411 + temperature: 0.3, 412 + }), 413 + }); 414 + 415 + if (response.status !== 200) { 416 + throw new Error( 417 + `API returned status code ${response.status}: ${JSON.stringify( 418 + response.json 419 + )}` 420 + ); 421 + } 422 + 423 + tagText = response.json.choices[0].message.content; 424 + break; 425 + 426 + case "google": 427 + // Google Gemini has a different API format 428 + const apiEndpoint = `${providerConfig.apiUrl}${this.settings.modelName}:generateContent?key=${this.settings.apiKey}`; 429 + 430 + response = await requestUrl({ 431 + url: apiEndpoint, 432 + method: "POST", 433 + headers: { 434 + "Content-Type": "application/json", 435 + }, 436 + body: JSON.stringify({ 437 + contents: [ 438 + { 439 + role: "user", 440 + parts: [{ text: prompt }], 441 + }, 442 + ], 443 + generationConfig: { 444 + temperature: 0.2, 445 + maxOutputTokens: 1000, 446 + }, 447 + }), 448 + }); 449 + 450 + if (response.status !== 200) { 451 + throw new Error( 452 + `API returned status code ${response.status}: ${JSON.stringify( 453 + response.json 454 + )}` 455 + ); 456 + } 457 + 458 + tagText = response.json.candidates[0].content.parts[0].text; 459 + break; 460 + 461 + default: 462 + throw new Error(`Unknown provider: ${this.settings.provider}`); 256 463 } 257 464 258 - const result = response.json; 259 - const tagText = result.content[0].text; 260 - 261 465 // Split by commas and trim whitespace 262 466 return tagText 263 467 .split(",") 264 468 .map((tag: string) => tag.trim()) 265 469 .filter((tag: string) => tag.length > 0); 266 470 } catch (error) { 267 - console.error("Error calling Claude API:", error); 471 + console.error(`Error calling ${this.settings.provider} API:`, error); 268 472 const errorMessage = error instanceof Error ? error.message : String(error); 269 473 throw new Error(`Failed to generate tags: ${errorMessage}`); 270 474 } ··· 386 590 387 591 containerEl.empty(); 388 592 593 + // AI Provider selection 594 + new Setting(containerEl) 595 + .setName("AI Provider") 596 + .setDesc("Select which AI provider to use for generating tags.") 597 + .addDropdown((dropdown) => { 598 + dropdown 599 + .addOption("anthropic", "Anthropic (Claude)") 600 + .addOption("openai", "OpenAI (GPT)") 601 + .addOption("mistral", "Mistral AI") 602 + .addOption("google", "Google (Gemini)") 603 + .addOption("custom", "Custom Endpoint (OpenAI Compatible)") 604 + .setValue(this.plugin.settings.provider) 605 + .onChange(async (value) => { 606 + const newProvider = value as AIProvider; 607 + this.plugin.settings.provider = newProvider; 608 + 609 + // Update model to default for the selected provider 610 + if (newProvider !== "custom") { 611 + this.plugin.settings.modelName = MODEL_CONFIGS[newProvider].defaultModel; 612 + } 613 + 614 + await this.plugin.saveSettings(); 615 + this.display(); // Refresh to show provider-specific options 616 + }); 617 + return dropdown; 618 + }); 619 + 620 + // Custom endpoint setting (only shown for custom provider) 621 + if (this.plugin.settings.provider === "custom") { 622 + new Setting(containerEl) 623 + .setName("Custom API Endpoint") 624 + .setDesc("Enter the URL for your custom OpenAI-compatible API endpoint.") 625 + .addText((text) => 626 + text 627 + .setPlaceholder("https://your-api-endpoint.com/v1/chat/completions") 628 + .setValue(this.plugin.settings.customEndpoint) 629 + .onChange(async (value) => { 630 + this.plugin.settings.customEndpoint = value; 631 + await this.plugin.saveSettings(); 632 + }) 633 + ); 634 + } 635 + 636 + // Get the current provider config 637 + const providerConfig = MODEL_CONFIGS[this.plugin.settings.provider]; 638 + 639 + // API Key with provider-specific description 389 640 new Setting(containerEl) 390 641 .setName("API key") 391 642 .setDesc( 392 - "Your Anthropic API key. Required to use the AI service. Get it from https://console.anthropic.com/ if you don't have one already. We recommend using a dedicated key for this plugin." 643 + `Your ${this.plugin.settings.provider === "custom" ? "" : this.plugin.settings.provider} API key. Required to use the AI service. ${ 644 + providerConfig.apiKeyUrl ? `Get it from ${providerConfig.apiKeyUrl} if you don't have one already.` : "" 645 + } We recommend using a dedicated key for this plugin.` 393 646 ) 394 647 .addText((text) => 395 648 text ··· 401 654 }) 402 655 ); 403 656 657 + // AI model selection (provider-specific) 404 658 new Setting(containerEl) 405 659 .setName("AI model") 406 660 .setDesc("Choose which AI model to use for tag generation.") 407 - .addDropdown((dropdown) => 408 - dropdown 409 - .addOption("claude-3-5-sonnet-20240620", "Claude 3.5 Sonnet") 410 - .addOption("claude-3-opus-20240229", "Claude 3 Opus") 411 - .addOption("claude-3-sonnet-20240229", "Claude 3 Sonnet") 412 - .addOption("claude-3-haiku-20240307", "Claude 3 Haiku") 413 - .setValue(this.plugin.settings.modelName) 661 + .addDropdown((dropdown) => { 662 + // Add models for the selected provider 663 + providerConfig.models.forEach(model => { 664 + dropdown.addOption(model.id, model.name); 665 + }); 666 + 667 + // If current model isn't in the list, add it 668 + if (!providerConfig.models.some(m => m.id === this.plugin.settings.modelName)) { 669 + dropdown.addOption(this.plugin.settings.modelName, this.plugin.settings.modelName + " (Custom)"); 670 + } 671 + 672 + dropdown.setValue(this.plugin.settings.modelName) 414 673 .onChange(async (value) => { 415 674 this.plugin.settings.modelName = value; 416 675 await this.plugin.saveSettings(); 417 - }) 418 - ); 676 + }); 677 + 678 + return dropdown; 679 + }); 419 680 420 681 new Setting(containerEl) 421 682 .setName("Maximum number of tags")