feat: multi-provider AI support with per-provider model selection

- Add llm-factory.ts: unified LLM provider abstraction (Google/Claude/OpenAI)
- Install @langchain/anthropic and @langchain/openai packages
- resolveAiProvider picks active provider from DB settings (Claude > OpenAI > Google)
- All AI graphs (reminder, new-patient, reschedule, internal-chat) now accept provider+model params
- Add claudeAiModel, openAiModel, googleAiModel columns to ai_settings table
- New PUT /api/ai/provider-model route to save selected model per provider
- UI model dropdowns for Claude (Haiku/Sonnet/Opus), OpenAI (GPT-5.x series), Google (Gemini 2.5/3.x)
- Google AI section also gets model selector alongside existing API key field

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
ff
2026-06-06 09:34:11 -04:00
co-authored by Claude Sonnet 4.6
parent d5bc96ff39
commit 4899ab8368
57 changed files with 681 additions and 138 deletions
+32 -4
View File
@@ -2,6 +2,7 @@ import express, { Request, Response } from "express";
import { storage } from "../storage";
import { classifyInternalChat } from "../ai/internal-chat-graph";
import { runInternalChatWorkflow } from "../ai/internal-chat-workflow";
import { resolveAiProvider } from "../ai/llm-factory";
const router = express.Router();
@@ -20,8 +21,11 @@ router.get("/settings", async (req: Request, res: Response): Promise<any> => {
aiEnabled: settings.aiEnabled ?? true,
openAiKey: settings.openAiKey ?? "",
openAiEnabled: settings.openAiEnabled ?? false,
openAiModel: settings.openAiModel ?? "gpt-5.2",
claudeAiKey: settings.claudeAiKey ?? "",
claudeAiEnabled: settings.claudeAiEnabled ?? false,
claudeAiModel: settings.claudeAiModel ?? "claude-haiku-4-5-20251001",
googleAiModel: settings.googleAiModel ?? "gemini-2.5-flash",
dentalMgmtKey: settings.dentalMgmtKey ?? "",
dentalMgmtEnabled: settings.dentalMgmtEnabled ?? false,
openPhoneReply: settings.openPhoneReply ?? false,
@@ -109,6 +113,27 @@ router.put("/provider-enabled", async (req: Request, res: Response): Promise<any
}
});
// PUT /api/ai/provider-model
router.put("/provider-model", async (req: Request, res: Response): Promise<any> => {
try {
const userId = req.user?.id;
if (!userId) return res.status(401).json({ message: "Unauthorized" });
const { provider, model } = req.body;
if (!["claudeAi", "openAi", "googleAi"].includes(provider)) {
return res.status(400).json({ message: "Invalid provider" });
}
if (!model?.trim()) {
return res.status(400).json({ message: "model is required" });
}
await storage.setProviderModel(userId, provider, model.trim());
return res.status(200).json({ provider, model: model.trim() });
} catch (err) {
return res.status(500).json({ error: "Failed to save provider model", details: String(err) });
}
});
// GET /api/ai/advanced-settings
router.get("/advanced-settings", async (req: Request, res: Response): Promise<any> => {
try {
@@ -236,9 +261,10 @@ router.post("/internal-chat", async (req: Request, res: Response): Promise<any>
if (!message?.trim()) return res.status(400).json({ message: "message is required" });
const aiSettings = await storage.getAiSettings(userId);
if (!aiSettings?.apiKey) {
const activeAi = resolveAiProvider(aiSettings ?? {});
if (!activeAi) {
return res.status(200).json({
reply: "AI is not configured. Please add a Google AI API key in Settings.",
reply: "AI is not configured. Please add an API key in AI Settings.",
});
}
@@ -249,9 +275,11 @@ router.post("/internal-chat", async (req: Request, res: Response): Promise<any>
const classification = await classifyInternalChat(
message.trim(),
aiSettings.apiKey,
activeAi.key,
extraSystemPrompt || undefined,
Array.isArray(history) ? history : []
Array.isArray(history) ? history : [],
activeAi.provider,
activeAi.model
);
const response = await runInternalChatWorkflow(classification, userId, storage, customAliases);