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.
This commit is contained in:
2026-09-07 17:14:33 -04:00
parent 3abd9d9b3b
commit 9b7ec36828
9 changed files with 270 additions and 207 deletions
+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);
}