mirror of
https://github.com/zadam/trilium.git
synced 2026-06-26 17:30:38 +02:00
feat(llm): integrate API keys with provider settings
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import type { Request, Response } from "express";
|
||||
import type { LlmMessage } from "@triliumnext/commons";
|
||||
|
||||
import { getProvider, type LlmProviderConfig } from "../../services/llm/index.js";
|
||||
import { getProviderByType, hasConfiguredProviders, type LlmProviderConfig } from "../../services/llm/index.js";
|
||||
import { streamToChunks } from "../../services/llm/stream.js";
|
||||
|
||||
interface ChatRequest {
|
||||
@@ -43,7 +43,13 @@ async function streamChat(req: Request, res: Response) {
|
||||
const flushableRes = res as Response & { flush?: () => void };
|
||||
|
||||
try {
|
||||
const provider = getProvider(config.provider || "anthropic");
|
||||
if (!hasConfiguredProviders()) {
|
||||
res.write(`data: ${JSON.stringify({ type: "error", error: "No LLM providers configured. Please add a provider in Options → AI / LLM." })}\n\n`);
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
const provider = getProviderByType(config.provider || "anthropic");
|
||||
const result = provider.chat(messages, config);
|
||||
|
||||
// Get pricing from provider for cost calculation
|
||||
@@ -68,10 +74,15 @@ async function streamChat(req: Request, res: Response) {
|
||||
* Get available models for a provider.
|
||||
*/
|
||||
function getModels(req: Request, res: Response) {
|
||||
const provider = req.query.provider as string || "anthropic";
|
||||
const providerType = req.query.provider as string || "anthropic";
|
||||
|
||||
try {
|
||||
const llmProvider = getProvider(provider);
|
||||
if (!hasConfiguredProviders()) {
|
||||
res.status(400).json({ error: "No LLM providers configured. Please add a provider in Options → AI / LLM." });
|
||||
return;
|
||||
}
|
||||
|
||||
const llmProvider = getProviderByType(providerType);
|
||||
const models = llmProvider.getAvailableModels();
|
||||
res.json({ models });
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,24 +1,103 @@
|
||||
import type { LlmProvider } from "./types.js";
|
||||
import { AnthropicProvider } from "./providers/anthropic.js";
|
||||
import optionService from "../options.js";
|
||||
import log from "../log.js";
|
||||
|
||||
const providers: Record<string, () => LlmProvider> = {
|
||||
anthropic: () => new AnthropicProvider()
|
||||
// Future providers can be added here
|
||||
};
|
||||
|
||||
let cachedProviders: Record<string, LlmProvider> = {};
|
||||
|
||||
export function getProvider(name: string = "anthropic"): LlmProvider {
|
||||
if (!cachedProviders[name]) {
|
||||
const factory = providers[name];
|
||||
if (!factory) {
|
||||
throw new Error(`Unknown LLM provider: ${name}. Available: ${Object.keys(providers).join(", ")}`);
|
||||
}
|
||||
cachedProviders[name] = factory();
|
||||
}
|
||||
return cachedProviders[name];
|
||||
/**
|
||||
* Configuration for a single LLM provider instance.
|
||||
* This matches the structure stored in the llmProviders option.
|
||||
*/
|
||||
export interface LlmProviderSetup {
|
||||
id: string;
|
||||
name: string;
|
||||
provider: string;
|
||||
apiKey: string;
|
||||
}
|
||||
|
||||
/** Factory functions for creating provider instances */
|
||||
const providerFactories: Record<string, (apiKey: string) => LlmProvider> = {
|
||||
anthropic: (apiKey) => new AnthropicProvider(apiKey)
|
||||
};
|
||||
|
||||
/** Cache of instantiated providers by their config ID */
|
||||
let cachedProviders: Record<string, LlmProvider> = {};
|
||||
|
||||
/**
|
||||
* Get configured providers from the options.
|
||||
*/
|
||||
function getConfiguredProviders(): LlmProviderSetup[] {
|
||||
try {
|
||||
const providersJson = optionService.getOptionOrNull("llmProviders");
|
||||
if (!providersJson) {
|
||||
return [];
|
||||
}
|
||||
return JSON.parse(providersJson) as LlmProviderSetup[];
|
||||
} catch (e) {
|
||||
log.error(`Failed to parse llmProviders option: ${e}`);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a provider instance by its configuration ID.
|
||||
* If no ID is provided, returns the first configured provider.
|
||||
*/
|
||||
export function getProvider(providerId?: string): LlmProvider {
|
||||
const configs = getConfiguredProviders();
|
||||
|
||||
if (configs.length === 0) {
|
||||
throw new Error("No LLM providers configured. Please add a provider in Options → AI / LLM.");
|
||||
}
|
||||
|
||||
// Find the requested provider or use the first one
|
||||
const config = providerId
|
||||
? configs.find(c => c.id === providerId)
|
||||
: configs[0];
|
||||
|
||||
if (!config) {
|
||||
throw new Error(`LLM provider not found: ${providerId}`);
|
||||
}
|
||||
|
||||
// Check cache
|
||||
if (cachedProviders[config.id]) {
|
||||
return cachedProviders[config.id];
|
||||
}
|
||||
|
||||
// Create new provider instance
|
||||
const factory = providerFactories[config.provider];
|
||||
if (!factory) {
|
||||
throw new Error(`Unknown LLM provider type: ${config.provider}. Available: ${Object.keys(providerFactories).join(", ")}`);
|
||||
}
|
||||
|
||||
const provider = factory(config.apiKey);
|
||||
cachedProviders[config.id] = provider;
|
||||
return provider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the first configured provider of a specific type (e.g., "anthropic").
|
||||
*/
|
||||
export function getProviderByType(providerType: string): LlmProvider {
|
||||
const configs = getConfiguredProviders();
|
||||
const config = configs.find(c => c.provider === providerType);
|
||||
|
||||
if (!config) {
|
||||
throw new Error(`No ${providerType} provider configured. Please add one in Options → AI / LLM.`);
|
||||
}
|
||||
|
||||
return getProvider(config.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if any providers are configured.
|
||||
*/
|
||||
export function hasConfiguredProviders(): boolean {
|
||||
return getConfiguredProviders().length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the provider cache. Call this when provider configurations change.
|
||||
*/
|
||||
export function clearProviderCache(): void {
|
||||
cachedProviders = {};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { anthropic } from "@ai-sdk/anthropic";
|
||||
import { createAnthropic, type AnthropicProvider as AnthropicSDKProvider } from "@ai-sdk/anthropic";
|
||||
import { streamText, stepCountIs, type CoreMessage } from "ai";
|
||||
import type { LlmMessage } from "@triliumnext/commons";
|
||||
|
||||
@@ -133,13 +133,13 @@ function buildNoteContext(noteId: string): string | null {
|
||||
|
||||
export class AnthropicProvider implements LlmProvider {
|
||||
name = "anthropic";
|
||||
private anthropic: AnthropicSDKProvider;
|
||||
|
||||
constructor() {
|
||||
const apiKey = process.env.ANTHROPIC_API_KEY;
|
||||
constructor(apiKey: string) {
|
||||
if (!apiKey) {
|
||||
throw new Error("ANTHROPIC_API_KEY environment variable is required");
|
||||
throw new Error("API key is required for Anthropic provider");
|
||||
}
|
||||
// The anthropic provider reads ANTHROPIC_API_KEY from env automatically
|
||||
this.anthropic = createAnthropic({ apiKey });
|
||||
}
|
||||
|
||||
chat(messages: LlmMessage[], config: LlmProviderConfig): StreamResult {
|
||||
@@ -162,7 +162,7 @@ export class AnthropicProvider implements LlmProvider {
|
||||
content: m.content
|
||||
}));
|
||||
|
||||
const model = anthropic(config.model || DEFAULT_MODEL);
|
||||
const model = this.anthropic(config.model || DEFAULT_MODEL);
|
||||
|
||||
// Build options for streamText
|
||||
const streamOptions: Parameters<typeof streamText>[0] = {
|
||||
@@ -193,7 +193,7 @@ export class AnthropicProvider implements LlmProvider {
|
||||
const tools: Record<string, unknown> = {};
|
||||
|
||||
if (config.enableWebSearch) {
|
||||
tools.web_search = anthropic.tools.webSearch_20250305({
|
||||
tools.web_search = this.anthropic.tools.webSearch_20250305({
|
||||
maxUses: 5
|
||||
});
|
||||
}
|
||||
|
||||
@@ -209,7 +209,10 @@ const defaultOptions: DefaultOption[] = [
|
||||
]),
|
||||
isSynced: true
|
||||
},
|
||||
{ name: "experimentalFeatures", value: "[]", isSynced: true }
|
||||
{ name: "experimentalFeatures", value: "[]", isSynced: true },
|
||||
|
||||
// AI / LLM
|
||||
{ name: "llmProviders", value: "[]", isSynced: false }
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user