From 9b7ec36828fe004604cd90d4fd54214ded14ee41 Mon Sep 17 00:00:00 2001 From: Olivier Date: Mon, 7 Sep 2026 17:14:33 -0400 Subject: [PATCH] Unify AI provider selection across all features Every LLM-backed feature (Post-Processing, Proofreading, Title Generation, Property Filler) now reads from one CHAT_PROVIDERS registry instead of 5 hand-duplicated provider lists, so they always offer the exact same providers in the exact same order. - Add Groq as a 5th chat-completion provider (was transcription-only despite its OpenAI-compatible API and an already-collected key). - Provider dropdowns disable options with no API key configured (shown as e.g. "Anthropic (no API key)") instead of silently failing at runtime when picked. - Collapse 3 byte-for-byte duplicated resolveProvider() methods (Proofreader, TitleGenerator, PropertyFiller) plus AudioHandler's getPostProcessingApiKey() into one resolveChatProvider() helper (src/ProviderResolver.ts). - Fix Custom provider: previously Proofreading/Title/Property silently reused Post-Processing's postProcessingUrl with no visible field to set it. Renamed to a shared customApiUrl field with its own UI (createCustomProviderFields), now exposed on every tab that offers Custom, plus rows on the API Keys tab. Migrates old data.json values automatically. - Update README settings reference and getting-started sections to match. No behavior change for existing single-provider setups; migration handles the postProcessingUrl -> customApiUrl rename transparently. --- README.md | 22 ++- src/AIService.ts | 2 +- src/NibbleAISettingsTab.ts | 226 +++++++++++++++++++----------- src/ProviderResolver.ts | 27 ++++ src/SettingsManager.ts | 62 ++++++-- src/proofreading/Proofreader.ts | 34 +---- src/property/PropertyFiller.ts | 32 +---- src/title/TitleGenerator.ts | 32 +---- src/transcription/AudioHandler.ts | 40 +++--- 9 files changed, 270 insertions(+), 207 deletions(-) create mode 100644 src/ProviderResolver.ts diff --git a/README.md b/README.md index b1a8aad..9a4b15c 100644 --- a/README.md +++ b/README.md @@ -75,11 +75,15 @@ Open **Settings** → **NibbleAI** and add at least one API key: | Key | Used For | |-----|----------| -| **Transcription API Key** | Whisper transcription (OpenAI, Groq, Azure) | -| **OpenAI API Key** | Proofreading, title generation, property filling, post-processing *(recommended: OpenRouter)* | -| **Anthropic API Key** | Post-processing with Claude models | +| **Groq API Key** | Whisper transcription, and as a chat provider for proofreading/title/property/post-processing | +| **OpenAI API Key** | Whisper transcription, and as a chat provider for proofreading/title/property/post-processing | +| **Anthropic API Key** | Claude models for proofreading/title/property/post-processing | +| **OpenRouter API Key** | Access to hundreds of models for proofreading/title/property/post-processing | +| **Custom API Key + URL** | Any OpenAI-compatible endpoint (self-hosted, Azure, etc.) | -> **Tip:** For proofreading, title generation, and property filling, an [OpenRouter](https://openrouter.ai/keys) API key with a free model like `google/gemini-2.5-flash-lite` costs nothing and works great. +Every LLM-backed feature (Proofreading, Title Generation, Property Filler, Post-Processing) shows the same provider dropdown — pick any provider that has a key configured above. + +> **Tip:** An [OpenRouter](https://openrouter.ai/keys) API key with a free model like `google/gemini-2.5-flash-lite` costs nothing and works great across all four AI features. ### 2. Try the Features @@ -172,13 +176,14 @@ Securely stored in your system keychain via Obsidian's SecretStorage. Never writ | Setting | Default | Description | |---------|---------|-------------| | Enable | Off | Clean up transcripts with an LLM | -| Provider | Anthropic | Anthropic, OpenAI, or Custom | +| Provider | Anthropic | Any configured provider: Anthropic, OpenAI, Groq, OpenRouter, or Custom | | Model | `claude-sonnet-4-20250514` | LLM for cleanup | | Auto-generate title | Off | Create filenames from content | ### Proofreading | Setting | Default | Description | |---------|---------|-------------| +| Provider | OpenRouter | Any configured provider: Anthropic, OpenAI, Groq, OpenRouter, or Custom | | Model | `openai/gpt-4o-mini` | Model for proofreading | | Temperature | `0.2` | Lower = more consistent | | Max tokens | `2000` | Maximum response length | @@ -187,12 +192,16 @@ Securely stored in your system keychain via Obsidian's SecretStorage. Never writ ### Title Generation | Setting | Default | Description | |---------|---------|-------------| +| Provider | OpenRouter | Any configured provider: Anthropic, OpenAI, Groq, OpenRouter, or Custom | +| Model | `openai/gpt-4o-mini` | Model for generating titles | | Mode | Rename file | Rename file or set frontmatter property | | Max title length | 8 words | Maximum words in generated title | ### Property Filler | Setting | Default | Description | |---------|---------|-------------| +| Provider | OpenRouter | Any configured provider: Anthropic, OpenAI, Groq, OpenRouter, or Custom | +| Model | `openai/gpt-4o-mini` | Model for generating property values | | Custom prompt | *(built-in)* | Template with `{property}` placeholder | | Overwrite existing | Off | Replace existing property values | @@ -205,7 +214,8 @@ NibbleAI/ ├── main.ts # Plugin entry point, commands, events ├── src/ │ ├── SettingsManager.ts # Settings + SecretStorage management -│ ├── AIService.ts # Unified AI client (OpenAI + Anthropic) +│ ├── AIService.ts # Unified AI client (OpenAI-compatible + Anthropic-native) +│ ├── ProviderResolver.ts # Shared provider → API key/endpoint resolution │ ├── ModelBrowser.ts # OpenRouter model suggest modal │ ├── PostProcessor.ts # LLM post-processing for transcripts │ ├── NibbleAISettingsTab.ts # Settings UI diff --git a/src/AIService.ts b/src/AIService.ts index 94f6110..c809a2e 100644 --- a/src/AIService.ts +++ b/src/AIService.ts @@ -5,7 +5,7 @@ export interface ChatCompletionOptions { apiKey: string; model: string; endpoint?: string; - provider?: "anthropic" | "openai" | "openrouter" | "custom"; + provider?: "anthropic" | "openai" | "openrouter" | "groq" | "custom"; temperature?: number; maxTokens?: number; } diff --git a/src/NibbleAISettingsTab.ts b/src/NibbleAISettingsTab.ts index ebe0147..47f7a4e 100644 --- a/src/NibbleAISettingsTab.ts +++ b/src/NibbleAISettingsTab.ts @@ -3,9 +3,8 @@ import { App, PluginSettingTab, Setting, setIcon } from "obsidian"; import { SettingsManager, PostProcessingProvider, - AIProvider, TranscriptionProvider, - PROVIDER_URLS, + CHAT_PROVIDERS, PROVIDER_DEFAULT_MODELS, } from "./SettingsManager"; import { ModelSuggestModal } from "./ModelBrowser"; @@ -106,10 +105,14 @@ export class NibbleAISettingsTab extends PluginSettingTab { text: "API keys are stored securely in your system keychain.", cls: "setting-item-description", }); + containerEl.createEl("p", { + text: "Each AI feature below (Proofreading, Title Generation, Property Filler, Post-Processing) lets you pick which of these providers to use, provided its key is set here.", + cls: "setting-item-description", + }); this.createApiKeySetting(containerEl, "Groq API Key", - "For Whisper transcription via Groq (groq.com)", + "For Whisper transcription and Groq chat models (groq.com)", "gsk_...", this.plugin.settings.groqApiKey, async (value) => { @@ -139,7 +142,7 @@ export class NibbleAISettingsTab extends PluginSettingTab { ); this.createApiKeySetting(containerEl, "OpenRouter API Key", - "For OpenRouter models — get one at openrouter.ai/keys. Used for proofreading, title generation, property filling, and post-processing.", + "For OpenRouter models — get one at openrouter.ai/keys.", "sk-or-v1-...", this.plugin.settings.openRouterApiKey, async (value) => { @@ -147,6 +150,26 @@ export class NibbleAISettingsTab extends PluginSettingTab { await this.save(); } ); + this.createApiKeySetting(containerEl, + "Custom API Key", + "For a self-hosted or OpenAI-compatible endpoint not listed above", + "sk-...xxxx", + this.plugin.settings.customApiKey, + async (value) => { + this.plugin.settings.customApiKey = value; + await this.save(); + } + ); + this.createTextSetting(containerEl, + "Custom API URL", + "Chat completions endpoint for the custom provider", + "https://api.example.com/v1/chat/completions", + this.plugin.settings.customApiUrl, + async (value) => { + this.plugin.settings.customApiUrl = value; + await this.save(); + } + ); } // ── Transcription Tab ── @@ -328,29 +351,22 @@ export class NibbleAISettingsTab extends PluginSettingTab { ); if (this.plugin.settings.postProcessing) { - this.createPostProcessingProviderSetting(containerEl); + this.createChatProviderSetting(containerEl, + "Select the LLM provider for cleaning up transcriptions", + this.plugin.settings.postProcessingProvider, + async (provider) => { + this.plugin.settings.postProcessingProvider = provider; + if (provider !== "custom") { + this.plugin.settings.postProcessingModel = + PROVIDER_DEFAULT_MODELS[provider]; + } + await this.save(); + this.display(); + } + ); if (this.plugin.settings.postProcessingProvider === "custom") { - this.createTextSetting(containerEl, - "API URL", - "Custom endpoint for chat completions", - "https://api.example.com/v1/chat/completions", - this.plugin.settings.postProcessingUrl, - async (value) => { - this.plugin.settings.postProcessingUrl = value; - await this.save(); - } - ); - this.createApiKeySetting(containerEl, - "API Key", - "API key for the custom endpoint", - "sk-...xxxx", - this.plugin.settings.customApiKey, - async (value) => { - this.plugin.settings.customApiKey = value; - await this.save(); - } - ); + this.createCustomProviderFields(containerEl); } this.createModelSetting(containerEl, @@ -424,15 +440,23 @@ export class NibbleAISettingsTab extends PluginSettingTab { cls: "setting-item-description", }); - this.createProviderSetting(containerEl, - "Provider", + this.createChatProviderSetting(containerEl, "Select the AI provider for proofreading", this.plugin.settings.proofreadingProvider, - async (value) => { - this.plugin.settings.proofreadingProvider = value as AIProvider; + async (provider) => { + this.plugin.settings.proofreadingProvider = provider; + if (provider !== "custom") { + this.plugin.settings.proofreadingModel = + PROVIDER_DEFAULT_MODELS[provider]; + } await this.save(); + this.display(); } ); + + if (this.plugin.settings.proofreadingProvider === "custom") { + this.createCustomProviderFields(containerEl); + } this.createModelSetting(containerEl, "Model", "Model for fixing punctuation and grammar", @@ -508,15 +532,23 @@ export class NibbleAISettingsTab extends PluginSettingTab { cls: "setting-item-description", }); - this.createProviderSetting(containerEl, - "Provider", + this.createChatProviderSetting(containerEl, "Select the AI provider for title generation", this.plugin.settings.titleGenProvider, - async (value) => { - this.plugin.settings.titleGenProvider = value as AIProvider; + async (provider) => { + this.plugin.settings.titleGenProvider = provider; + if (provider !== "custom") { + this.plugin.settings.titleGenModel = + PROVIDER_DEFAULT_MODELS[provider]; + } await this.save(); + this.display(); } ); + + if (this.plugin.settings.titleGenProvider === "custom") { + this.createCustomProviderFields(containerEl); + } this.createModelSetting(containerEl, "Model", "Model for generating titles", @@ -577,15 +609,23 @@ export class NibbleAISettingsTab extends PluginSettingTab { cls: "setting-item-description", }); - this.createProviderSetting(containerEl, - "Provider", + this.createChatProviderSetting(containerEl, "Select the AI provider for property generation", this.plugin.settings.propertyGenProvider, - async (value) => { - this.plugin.settings.propertyGenProvider = value as AIProvider; + async (provider) => { + this.plugin.settings.propertyGenProvider = provider; + if (provider !== "custom") { + this.plugin.settings.propertyGenModel = + PROVIDER_DEFAULT_MODELS[provider]; + } await this.save(); + this.display(); } ); + + if (this.plugin.settings.propertyGenProvider === "custom") { + this.createCustomProviderFields(containerEl); + } this.createModelSetting(containerEl, "Model", "Model for generating property values", @@ -748,30 +788,53 @@ export class NibbleAISettingsTab extends PluginSettingTab { infoEl.style.flex = "1 1 100%"; } - private createProviderSetting( + /** + * Shared provider dropdown for every LLM-backed feature (Post-Processing, + * Proofreading, Title Generation, Property Filler). Reads from the single + * CHAT_PROVIDERS registry so all four features always offer the exact + * same providers, and disables any provider whose API key isn't set yet. + */ + private createChatProviderSetting( containerEl: HTMLElement, - name: string, desc: string, - value: string, - onChange: (value: string) => Promise + currentProvider: PostProcessingProvider, + onChange: (provider: PostProcessingProvider) => Promise ): void { - const providers: Record = { - openrouter: "OpenRouter", - openai: "OpenAI", - anthropic: "Anthropic", - custom: "Custom", - }; + const configured = new Set( + CHAT_PROVIDERS.filter((p) => !!this.plugin.settings[p.apiKeyField]).map( + (p) => p.id + ) + ); new Setting(containerEl) - .setName(name) + .setName("Provider") .setDesc(desc) .addDropdown((dropdown) => { - for (const [val, label] of Object.entries(providers)) { - dropdown.addOption(val, label); + for (const p of CHAT_PROVIDERS) { + dropdown.addOption( + p.id, + configured.has(p.id) ? p.label : `${p.label} (no API key)` + ); + } + dropdown.setValue(currentProvider || "openrouter"); + dropdown.onChange(async (value) => { + await onChange(value as PostProcessingProvider); + }); + for (const p of CHAT_PROVIDERS) { + if (configured.has(p.id)) continue; + const optionEl = dropdown.selectEl.querySelector( + `option[value="${p.id}"]` + ) as HTMLOptionElement | null; + if (optionEl) optionEl.disabled = true; } - dropdown.setValue(value || "openrouter"); - dropdown.onChange(async (val) => await onChange(val)); }); + + if (configured.size === 0) { + containerEl.createEl("p", { + text: "⚠️ No AI provider configured yet — add an API key in the API Keys tab.", + cls: "setting-item-description", + }); + } } private createModelSetting( @@ -860,36 +923,35 @@ export class NibbleAISettingsTab extends PluginSettingTab { ); } - private createPostProcessingProviderSetting(containerEl: HTMLElement): void { - const providers: Record = { - anthropic: "Anthropic", - openai: "OpenAI", - openrouter: "OpenRouter", - custom: "Custom", - }; - - new Setting(containerEl) - .setName("Provider") - .setDesc("Select the LLM provider") - .addDropdown((dropdown) => { - for (const [value, label] of Object.entries(providers)) { - dropdown.addOption(value, label); - } - dropdown - .setValue(this.plugin.settings.postProcessingProvider) - .onChange(async (value) => { - const provider = value as PostProcessingProvider; - this.plugin.settings.postProcessingProvider = provider; - if (provider !== "custom") { - this.plugin.settings.postProcessingUrl = - PROVIDER_URLS[provider]; - this.plugin.settings.postProcessingModel = - PROVIDER_DEFAULT_MODELS[provider]; - } - await this.save(); - this.display(); - }); - }); + /** + * Custom endpoint + key fields, shown whenever a feature's provider is + * set to "Custom". This slot is shared across all four AI features. + */ + private createCustomProviderFields(containerEl: HTMLElement): void { + containerEl.createEl("p", { + text: "Custom provider connection \u2014 shared across Post-Processing, Proofreading, Title Generation, and Property Filler.", + cls: "setting-item-description", + }); + this.createTextSetting(containerEl, + "Custom API URL", + "OpenAI-compatible chat completions endpoint", + "https://api.example.com/v1/chat/completions", + this.plugin.settings.customApiUrl, + async (value) => { + this.plugin.settings.customApiUrl = value; + await this.save(); + } + ); + this.createApiKeySetting(containerEl, + "Custom API Key", + "API key for the custom endpoint", + "sk-...xxxx", + this.plugin.settings.customApiKey, + async (value) => { + this.plugin.settings.customApiKey = value; + await this.save(); + } + ); } private async createAudioDeviceSetting(containerEl: HTMLElement): Promise { diff --git a/src/ProviderResolver.ts b/src/ProviderResolver.ts new file mode 100644 index 0000000..418d783 --- /dev/null +++ b/src/ProviderResolver.ts @@ -0,0 +1,27 @@ +import { CHAT_PROVIDERS, PROVIDER_URLS, PluginSettings, PostProcessingProvider } from "./SettingsManager"; + +export interface ResolvedChatProvider { + apiKey: string; + endpoint: string; + provider: PostProcessingProvider; +} + +/** + * Resolves the API key + endpoint for a chat-completion provider selection. + * Shared by every AI feature that talks to an LLM (Post-Processing, + * Proofreading, Title Generation, Property Filler) so they all treat + * providers — including "no key configured" — identically. + */ +export function resolveChatProvider( + settings: PluginSettings, + provider: PostProcessingProvider +): ResolvedChatProvider | null { + const info = CHAT_PROVIDERS.find((p) => p.id === provider); + if (!info) return null; + + const apiKey = settings[info.apiKeyField]; + const endpoint = provider === "custom" ? settings.customApiUrl : PROVIDER_URLS[provider]; + + if (!apiKey || !endpoint) return null; + return { apiKey, endpoint, provider }; +} diff --git a/src/SettingsManager.ts b/src/SettingsManager.ts index f4e3dbb..20c909f 100644 --- a/src/SettingsManager.ts +++ b/src/SettingsManager.ts @@ -1,6 +1,6 @@ import { Plugin } from "obsidian"; -const SECRET_IDS: Record = { +const SECRET_IDS: Partial> = { groqApiKey: "groq-api-key", openAiApiKey: "openai-api-key", anthropicApiKey: "anthropic-api-key", @@ -8,7 +8,7 @@ const SECRET_IDS: Record = { openRouterApiKey: "openrouter-api-key", }; -export type PostProcessingProvider = "anthropic" | "openai" | "openrouter" | "custom"; +export type PostProcessingProvider = "anthropic" | "openai" | "openrouter" | "groq" | "custom"; export type AIProvider = PostProcessingProvider; @@ -16,6 +16,7 @@ export const PROVIDER_URLS: Record = { anthropic: "https://api.anthropic.com/v1/messages", openai: "https://api.openai.com/v1/chat/completions", openrouter: "https://openrouter.ai/api/v1/chat/completions", + groq: "https://api.groq.com/openai/v1/chat/completions", custom: "", }; @@ -23,9 +24,30 @@ export const PROVIDER_DEFAULT_MODELS: Record = { anthropic: "claude-sonnet-4-20250514", openai: "gpt-4o-mini", openrouter: "openai/gpt-4o-mini", + groq: "llama-3.3-70b-versatile", custom: "", }; +export interface ChatProviderInfo { + id: PostProcessingProvider; + label: string; + apiKeyField: keyof ApiKeysSettings; +} + +/** + * Single source of truth for which chat-completion providers exist and which + * API key each one uses. UI dropdowns and provider resolution both read from + * this list so every AI feature (Post-Processing, Proofreading, Title + * Generation, Property Filler) offers the exact same providers. + */ +export const CHAT_PROVIDERS: ChatProviderInfo[] = [ + { id: "anthropic", label: "Anthropic", apiKeyField: "anthropicApiKey" }, + { id: "openai", label: "OpenAI", apiKeyField: "openAiApiKey" }, + { id: "groq", label: "Groq", apiKeyField: "groqApiKey" }, + { id: "openrouter", label: "OpenRouter", apiKeyField: "openRouterApiKey" }, + { id: "custom", label: "Custom", apiKeyField: "customApiKey" }, +]; + export type TranscriptionProvider = "groq" | "openai" | "azure" | "custom"; export const TRANSCRIPTION_PROVIDER_URLS: Record = { @@ -42,6 +64,7 @@ export interface ApiKeysSettings { openAiApiKey: string; anthropicApiKey: string; customApiKey: string; + customApiUrl: string; openRouterApiKey: string; } @@ -74,7 +97,6 @@ export interface OutputSettings { export interface PostProcessingSettings { postProcessing: boolean; postProcessingProvider: PostProcessingProvider; - postProcessingUrl: string; postProcessingModel: string; postProcessingPrompt: string; autoGenerateTitle: boolean; @@ -128,6 +150,7 @@ export const DEFAULT_API_KEYS: ApiKeysSettings = { openAiApiKey: "", anthropicApiKey: "", customApiKey: "", + customApiUrl: "", openRouterApiKey: "", }; @@ -160,7 +183,6 @@ export const DEFAULT_OUTPUT: OutputSettings = { export const DEFAULT_POST_PROCESSING: PostProcessingSettings = { postProcessing: false, postProcessingProvider: "anthropic", - postProcessingUrl: "https://api.anthropic.com/v1/messages", postProcessingModel: "claude-sonnet-4-20250514", postProcessingPrompt: 'You are a transcription editor. Clean up the following voice transcription: fix grammar, remove filler words (um, uh, like) and repetitions, and improve readability. Format the text in markdown. If there are action items or to-dos, format them as task lists with "[ ]". Preserve the original meaning and language. Return only the polished text, nothing else.', @@ -274,19 +296,31 @@ export class SettingsManager { private migratePostProcessingProvider(settings: PluginSettings): boolean { if (settings.postProcessingProvider) return false; + const legacyUrl = (settings as PluginSettings & { postProcessingUrl?: string }) + .postProcessingUrl; + if (!legacyUrl) return false; for (const [provider, url] of Object.entries(PROVIDER_URLS)) { - if (url && settings.postProcessingUrl === url) { - settings.postProcessingProvider = - provider as PostProcessingProvider; + if (url && legacyUrl === url) { + settings.postProcessingProvider = provider as PostProcessingProvider; return true; } } - if (settings.postProcessingUrl) { - settings.postProcessingProvider = "custom"; - return true; - } - return false; + settings.postProcessingProvider = "custom"; + return true; + } + + private migrateCustomApiUrl(settings: PluginSettings): boolean { + // One-time migration: the old `postProcessingUrl` field is now the + // shared `customApiUrl` used by every feature's "Custom" provider. + if (settings.customApiUrl) return false; + const legacyUrl = (settings as PluginSettings & { postProcessingUrl?: string }) + .postProcessingUrl; + if (!legacyUrl) return false; + if (Object.values(PROVIDER_URLS).includes(legacyUrl)) return false; + + settings.customApiUrl = legacyUrl; + return true; } async loadSettings(): Promise { @@ -300,6 +334,10 @@ export class SettingsManager { await this.plugin.saveData(settings); } + if (this.migrateCustomApiUrl(settings)) { + await this.plugin.saveData(settings); + } + if (this.migrateKeysFromDataJson(settings)) { await this.plugin.saveData(settings); } diff --git a/src/proofreading/Proofreader.ts b/src/proofreading/Proofreader.ts index 5975f00..b958a5b 100644 --- a/src/proofreading/Proofreader.ts +++ b/src/proofreading/Proofreader.ts @@ -1,6 +1,6 @@ import { Editor, Notice } from "obsidian"; import { AIService } from "../AIService"; -import { AIProvider, PROVIDER_URLS } from "../SettingsManager"; +import { resolveChatProvider } from "../ProviderResolver"; import NibbleAI from "main"; export class Proofreader { @@ -10,33 +10,6 @@ export class Proofreader { this.plugin = plugin; } - private resolveProvider(): { apiKey: string; endpoint: string; provider: AIProvider } | null { - const provider = this.plugin.settings.proofreadingProvider || "openrouter"; - let apiKey = ""; - let endpoint = PROVIDER_URLS[provider] || ""; - - switch (provider) { - case "openai": - apiKey = this.plugin.settings.openAiApiKey; - break; - case "anthropic": - apiKey = this.plugin.settings.anthropicApiKey; - break; - case "openrouter": - apiKey = this.plugin.settings.openRouterApiKey; - break; - case "custom": - apiKey = this.plugin.settings.customApiKey; - endpoint = this.plugin.settings.postProcessingUrl || ""; - break; - } - - if (!apiKey) return null; - if (!endpoint) return null; - - return { apiKey, endpoint, provider }; - } - async fixSelection(editor: Editor): Promise { const selection = editor.getSelection(); if (!selection || selection.trim().length === 0) { @@ -44,7 +17,10 @@ export class Proofreader { return; } - const config = this.resolveProvider(); + const config = resolveChatProvider( + this.plugin.settings, + this.plugin.settings.proofreadingProvider || "openrouter" + ); if (!config) { new Notice( "⚠️ No API key for selected provider. Check Settings \u2192 NibbleAI \u2192 API Keys." diff --git a/src/property/PropertyFiller.ts b/src/property/PropertyFiller.ts index 4056551..925e46f 100644 --- a/src/property/PropertyFiller.ts +++ b/src/property/PropertyFiller.ts @@ -1,6 +1,6 @@ import { Notice, MarkdownView, Modal, App, Setting, TFile } from "obsidian"; import { AIService } from "../AIService"; -import { AIProvider, PROVIDER_URLS } from "../SettingsManager"; +import { resolveChatProvider } from "../ProviderResolver"; import NibbleAI from "main"; class PropertyPromptModal extends Modal { @@ -80,31 +80,6 @@ export class PropertyFiller { this.plugin = plugin; } - private resolveProvider(): { apiKey: string; endpoint: string; provider: AIProvider } | null { - const provider = this.plugin.settings.propertyGenProvider || "openrouter"; - let apiKey = ""; - let endpoint = PROVIDER_URLS[provider] || ""; - - switch (provider) { - case "openai": - apiKey = this.plugin.settings.openAiApiKey; - break; - case "anthropic": - apiKey = this.plugin.settings.anthropicApiKey; - break; - case "openrouter": - apiKey = this.plugin.settings.openRouterApiKey; - break; - case "custom": - apiKey = this.plugin.settings.customApiKey; - endpoint = this.plugin.settings.postProcessingUrl || ""; - break; - } - - if (!apiKey || !endpoint) return null; - return { apiKey, endpoint, provider }; - } - async fillProperty(): Promise { new PropertyPromptModal( this.plugin.app, @@ -139,7 +114,10 @@ export class PropertyFiller { return; } - const config = this.resolveProvider(); + const config = resolveChatProvider( + this.plugin.settings, + this.plugin.settings.propertyGenProvider || "openrouter" + ); if (!config) { new Notice( "⚠️ No API key for selected provider. Check Settings \u2192 NibbleAI \u2192 API Keys." diff --git a/src/title/TitleGenerator.ts b/src/title/TitleGenerator.ts index b54249d..11f33a0 100644 --- a/src/title/TitleGenerator.ts +++ b/src/title/TitleGenerator.ts @@ -1,6 +1,6 @@ import { Notice, TFile, MarkdownView } from "obsidian"; import { AIService } from "../AIService"; -import { AIProvider, PROVIDER_URLS } from "../SettingsManager"; +import { resolveChatProvider } from "../ProviderResolver"; import NibbleAI from "main"; export class TitleGenerator { @@ -10,31 +10,6 @@ export class TitleGenerator { this.plugin = plugin; } - private resolveProvider(): { apiKey: string; endpoint: string; provider: AIProvider } | null { - const provider = this.plugin.settings.titleGenProvider || "openrouter"; - let apiKey = ""; - let endpoint = PROVIDER_URLS[provider] || ""; - - switch (provider) { - case "openai": - apiKey = this.plugin.settings.openAiApiKey; - break; - case "anthropic": - apiKey = this.plugin.settings.anthropicApiKey; - break; - case "openrouter": - apiKey = this.plugin.settings.openRouterApiKey; - break; - case "custom": - apiKey = this.plugin.settings.customApiKey; - endpoint = this.plugin.settings.postProcessingUrl || ""; - break; - } - - if (!apiKey || !endpoint) return null; - return { apiKey, endpoint, provider }; - } - async generateTitle(): Promise { const activeView = this.plugin.app.workspace.getActiveViewOfType(MarkdownView); @@ -55,7 +30,10 @@ export class TitleGenerator { return; } - const config = this.resolveProvider(); + const config = resolveChatProvider( + this.plugin.settings, + this.plugin.settings.titleGenProvider || "openrouter" + ); if (!config) { new Notice( "⚠️ No API key for selected provider. Check Settings \u2192 NibbleAI \u2192 API Keys." diff --git a/src/transcription/AudioHandler.ts b/src/transcription/AudioHandler.ts index 49f500f..a6fbc48 100644 --- a/src/transcription/AudioHandler.ts +++ b/src/transcription/AudioHandler.ts @@ -8,6 +8,7 @@ import { resolveTemplate, } from "./utils"; import { PostProcessor } from "../PostProcessor"; +import { resolveChatProvider } from "../ProviderResolver"; export class AudioHandler { private plugin: NibbleAI; @@ -16,19 +17,6 @@ export class AudioHandler { this.plugin = plugin; } - private getPostProcessingApiKey(): string { - switch (this.plugin.settings.postProcessingProvider) { - case "anthropic": - return this.plugin.settings.anthropicApiKey; - case "openai": - return this.plugin.settings.openAiApiKey; - case "openrouter": - return this.plugin.settings.openRouterApiKey; - case "custom": - return this.plugin.settings.customApiKey; - } - } - private getTranscriptionUrl(): string { switch (this.plugin.settings.transcriptionProvider) { case "groq": @@ -173,8 +161,11 @@ export class AudioHandler { // Post-process with LLM if enabled if (this.plugin.settings.postProcessing) { - const ppApiKey = this.getPostProcessingApiKey(); - if (!ppApiKey) { + const resolved = resolveChatProvider( + this.plugin.settings, + this.plugin.settings.postProcessingProvider + ); + if (!resolved) { new Notice( "✘ Add your post-processing API key in settings" ); @@ -185,10 +176,10 @@ export class AudioHandler { new Notice("Post-processing..."); } const processor = new PostProcessor({ - apiKey: ppApiKey, + apiKey: resolved.apiKey, model: this.plugin.settings.postProcessingModel, - url: this.plugin.settings.postProcessingUrl, - provider: this.plugin.settings.postProcessingProvider, + url: resolved.endpoint, + provider: resolved.provider, }); finalText = await processor.process( originalText, @@ -209,14 +200,17 @@ export class AudioHandler { this.plugin.settings.autoGenerateTitle && this.plugin.settings.createNoteFile ) { - const ppApiKey = this.getPostProcessingApiKey(); - if (ppApiKey) { + const resolved = resolveChatProvider( + this.plugin.settings, + this.plugin.settings.postProcessingProvider + ); + if (resolved) { try { const processor = new PostProcessor({ - apiKey: ppApiKey, + apiKey: resolved.apiKey, model: this.plugin.settings.postProcessingModel, - url: this.plugin.settings.postProcessingUrl, - provider: this.plugin.settings.postProcessingProvider, + url: resolved.endpoint, + provider: resolved.provider, }); const title = await processor.process( finalText,