Files
nibbleai/src/AIService.ts
T
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

101 lines
2.6 KiB
TypeScript

import axios from "axios";
import { PostProcessingProvider } from "./SettingsManager";
export interface ChatCompletionOptions {
apiKey: string;
model: string;
endpoint?: string;
provider?: "anthropic" | "openai" | "openrouter" | "groq" | "custom";
temperature?: number;
maxTokens?: number;
}
/**
* Unified AI service for all NibbleAI modules.
* Supports OpenAI-compatible chat completions and Anthropic native API.
*/
export class AIService {
/**
* Call a chat completion API (OpenAI-compatible or Anthropic).
* @param userMessage - The user's input text
* @param systemPrompt - System-level instructions
* @param options - API configuration
* @returns The model's response text
*/
static async callChatCompletion(
userMessage: string,
systemPrompt: string,
options: ChatCompletionOptions
): Promise<string> {
const provider = options.provider || "openai";
const endpoint =
options.endpoint || "https://openrouter.ai/api/v1/chat/completions";
if (provider === "anthropic") {
return AIService.callAnthropic(userMessage, systemPrompt, options);
}
return AIService.callOpenAI(userMessage, systemPrompt, {
...options,
endpoint,
});
}
private static async callOpenAI(
userMessage: string,
systemPrompt: string,
options: ChatCompletionOptions
): Promise<string> {
const response = await axios.post(
options.endpoint ||
"https://openrouter.ai/api/v1/chat/completions",
{
model: options.model,
messages: [
{ role: "system", content: systemPrompt },
{ role: "user", content: userMessage },
],
temperature: options.temperature ?? 0.3,
max_tokens: options.maxTokens ?? 2000,
},
{
headers: {
Authorization: `Bearer ${options.apiKey}`,
"Content-Type": "application/json",
...(options.endpoint?.includes("openrouter.ai")
? {
"HTTP-Referer": "https://obsidian.md",
"X-Title": "Obsidian NibbleAI",
}
: {}),
},
}
);
return response.data.choices[0].message.content.trim();
}
private static async callAnthropic(
userMessage: string,
systemPrompt: string,
options: ChatCompletionOptions
): Promise<string> {
const response = await axios.post(
options.endpoint || "https://api.anthropic.com/v1/messages",
{
model: options.model,
max_tokens: options.maxTokens ?? 8192,
system: systemPrompt,
messages: [{ role: "user", content: userMessage }],
},
{
headers: {
"x-api-key": options.apiKey,
"anthropic-version": "2023-06-01",
"anthropic-dangerous-direct-browser-access": "true",
"Content-Type": "application/json",
},
}
);
return response.data.content[0].text;
}
}