2 Commits

Author SHA1 Message Date
olivier c6a98d4050 chore: bump to v1.2.0 2026-09-07 17:22:33 -04:00
olivier 9b7ec36828 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.
2026-09-07 17:14:33 -04:00
11 changed files with 272 additions and 209 deletions
+16 -6
View File
@@ -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
+1 -1
View File
@@ -1,7 +1,7 @@
{
"id": "nibbleai",
"name": "NibbleAI",
"version": "1.1.0",
"version": "1.2.0",
"minAppVersion": "1.12.7",
"description": "AI-powered tools for your Obsidian vault: speech-to-text, proofreading, title generation, property filling, and more.",
"author": "Olivier",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "nibbleai",
"version": "1.1.0",
"version": "1.2.0",
"description": "AI-powered tools for your Obsidian vault",
"main": "main.js",
"scripts": {
+1 -1
View File
@@ -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;
}
+144 -82
View File
@@ -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<void>
currentProvider: PostProcessingProvider,
onChange: (provider: PostProcessingProvider) => Promise<void>
): void {
const providers: Record<string, string> = {
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<PostProcessingProvider, string> = {
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<void> {
+27
View File
@@ -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 };
}
+50 -12
View File
@@ -1,6 +1,6 @@
import { Plugin } from "obsidian";
const SECRET_IDS: Record<keyof ApiKeysSettings, string> = {
const SECRET_IDS: Partial<Record<keyof ApiKeysSettings, string>> = {
groqApiKey: "groq-api-key",
openAiApiKey: "openai-api-key",
anthropicApiKey: "anthropic-api-key",
@@ -8,7 +8,7 @@ const SECRET_IDS: Record<keyof ApiKeysSettings, string> = {
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<PostProcessingProvider, string> = {
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<PostProcessingProvider, string> = {
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<TranscriptionProvider, string> = {
@@ -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<PluginSettings> {
@@ -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);
}
+5 -29
View File
@@ -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<void> {
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."
+5 -27
View File
@@ -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<void> {
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."
+5 -27
View File
@@ -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<void> {
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."
+17 -23
View File
@@ -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,