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
parent d5bc96ff39
commit 4899ab8368
57 changed files with 681 additions and 138 deletions

View File

@@ -1,5 +1,5 @@
import { StateGraph, END, START, Annotation } from "@langchain/langgraph";
import { ChatGoogleGenerativeAI } from "@langchain/google-genai";
import { getLlm, type AiProvider } from "./llm-factory";
const GraphState = Annotation.Root({
message: Annotation<string>(),
@@ -95,7 +95,9 @@ async function confirmNode(state: GraphStateType, config: any) {
if (!apiKey) return { reply: fallback };
try {
const llm = new ChatGoogleGenerativeAI({ model: "gemini-1.5-flash", apiKey });
const provider: AiProvider = config?.configurable?.provider ?? "google";
const model: string | undefined = config?.configurable?.model;
const llm = getLlm(provider, apiKey, model);
const apptClause = appt ? ` Their appointment is on ${appt}.` : "";
const response = await llm.invoke([
{
@@ -134,7 +136,9 @@ async function rescheduleNode(state: GraphStateType, config: any) {
if (!apiKey) return { reply: fallback };
try {
const llm = new ChatGoogleGenerativeAI({ model: "gemini-1.5-flash", apiKey });
const provider: AiProvider = config?.configurable?.provider ?? "google";
const model: string | undefined = config?.configurable?.model;
const llm = getLlm(provider, apiKey, model);
const response = await llm.invoke([
{
role: "system",
@@ -160,7 +164,9 @@ async function otherNode(state: GraphStateType, config: any) {
const fallback = NEW_APPT_FALLBACKS[lang] ?? NEW_APPT_FALLBACKS["English"]!;
if (!apiKey) return { reply: fallback, intent: "wants_appointment" };
try {
const llm = new ChatGoogleGenerativeAI({ model: "gemini-1.5-flash", apiKey });
const provider: AiProvider = config?.configurable?.provider ?? "google";
const model: string | undefined = config?.configurable?.model;
const llm = getLlm(provider, apiKey, model);
const response = await llm.invoke([
{
role: "system",
@@ -177,7 +183,9 @@ async function otherNode(state: GraphStateType, config: any) {
const fallback = state.generalFallback || (GENERAL_FALLBACKS[lang] ?? GENERAL_FALLBACKS["English"]!);
if (!apiKey) return { reply: fallback };
try {
const llm = new ChatGoogleGenerativeAI({ model: "gemini-1.5-flash", apiKey });
const provider: AiProvider = config?.configurable?.provider ?? "google";
const model: string | undefined = config?.configurable?.model;
const llm = getLlm(provider, apiKey, model);
const response = await llm.invoke([
{
role: "system",
@@ -215,11 +223,13 @@ export async function runReminderGraph(
language = "English",
appointmentDatetime = "",
rescheduleGreeting = "",
generalFallback = ""
generalFallback = "",
provider: AiProvider = "google",
model?: string
): Promise<{ reply: string | null; intent: string | null }> {
const result = await graph.invoke(
{ message: patientMessage, intent: "", reply: "", language, appointmentDatetime, rescheduleGreeting, generalFallback },
{ configurable: { apiKey } }
{ configurable: { apiKey, provider, model } }
);
return {
reply: result.reply || null,