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:
@@ -13,13 +13,17 @@ type AiSettings = {
|
||||
aiEnabled: boolean;
|
||||
openAiKey: string;
|
||||
openAiEnabled: boolean;
|
||||
openAiModel: string;
|
||||
claudeAiKey: string;
|
||||
claudeAiEnabled: boolean;
|
||||
claudeAiModel: string;
|
||||
googleAiModel: string;
|
||||
dentalMgmtKey: string;
|
||||
dentalMgmtEnabled: boolean;
|
||||
};
|
||||
|
||||
type Provider = "openAi" | "claudeAi" | "dentalMgmt";
|
||||
type ModelProvider = "claudeAi" | "openAi" | "googleAi";
|
||||
|
||||
function ApiKeySection({
|
||||
title,
|
||||
@@ -31,6 +35,9 @@ function ApiKeySection({
|
||||
onToggle,
|
||||
isSaving,
|
||||
isToggling,
|
||||
modelOptions,
|
||||
selectedModel,
|
||||
onModelChange,
|
||||
}: {
|
||||
title: string;
|
||||
description: string;
|
||||
@@ -41,6 +48,9 @@ function ApiKeySection({
|
||||
onToggle: (enabled: boolean) => void;
|
||||
isSaving: boolean;
|
||||
isToggling: boolean;
|
||||
modelOptions?: { value: string; label: string }[];
|
||||
selectedModel?: string;
|
||||
onModelChange?: (model: string) => void;
|
||||
}) {
|
||||
const [localKey, setLocalKey] = useState(apiKey);
|
||||
const [showKey, setShowKey] = useState(false);
|
||||
@@ -68,6 +78,21 @@ function ApiKeySection({
|
||||
|
||||
<p className="text-sm text-gray-500">{description}</p>
|
||||
|
||||
{modelOptions && onModelChange && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">Model</label>
|
||||
<select
|
||||
value={selectedModel}
|
||||
onChange={(e) => onModelChange(e.target.value)}
|
||||
className="p-2 border rounded w-full text-sm bg-white"
|
||||
>
|
||||
{modelOptions.map((opt) => (
|
||||
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium">API Key</label>
|
||||
<div className="relative mt-1">
|
||||
@@ -168,6 +193,21 @@ export function AiSettingsCard() {
|
||||
onError: (err: any) => toast({ title: "Error", description: err?.message, variant: "destructive" }),
|
||||
});
|
||||
|
||||
// Provider model change
|
||||
const providerModelMutation = useMutation({
|
||||
mutationFn: async ({ provider, model }: { provider: ModelProvider; model: string }) => {
|
||||
const res = await apiRequest("PUT", "/api/ai/provider-model", { provider, model });
|
||||
if (!res.ok) throw new Error((await res.json().catch(() => null))?.message || "Failed");
|
||||
return { provider, model };
|
||||
},
|
||||
onSuccess: ({ provider, model }) => {
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/ai/settings"] });
|
||||
const names: Record<string, string> = { claudeAi: "Claude AI", openAi: "OpenAI" };
|
||||
toast({ title: "Model updated", description: `${names[provider]} model set to ${model}.` });
|
||||
},
|
||||
onError: (err: any) => toast({ title: "Error", description: err?.message, variant: "destructive" }),
|
||||
});
|
||||
|
||||
// Provider toggle
|
||||
const providerToggleMutation = useMutation({
|
||||
mutationFn: async ({ provider, enabled }: { provider: Provider; enabled: boolean }) => {
|
||||
@@ -183,6 +223,28 @@ export function AiSettingsCard() {
|
||||
onError: (err: any) => toast({ title: "Error", description: err?.message, variant: "destructive" }),
|
||||
});
|
||||
|
||||
const CLAUDE_MODELS = [
|
||||
{ value: "claude-haiku-4-5-20251001", label: "Claude Haiku 4.5 — Fast & affordable" },
|
||||
{ value: "claude-sonnet-4-6", label: "Claude Sonnet 4.6 — Balanced" },
|
||||
{ value: "claude-opus-4-8", label: "Claude Opus 4.8 — Most capable" },
|
||||
];
|
||||
|
||||
const OPENAI_MODELS = [
|
||||
{ value: "gpt-5.2", label: "GPT-5.2 — Standard (recommended)" },
|
||||
{ value: "gpt-5.2-pro", label: "GPT-5.2 Pro — Professional grade" },
|
||||
{ value: "gpt-5.4", label: "GPT-5.4 — Previous gen, high quality" },
|
||||
{ value: "gpt-5.4-pro", label: "GPT-5.4 Pro — Previous gen pro" },
|
||||
{ value: "gpt-5.5", label: "GPT-5.5 — Flagship, complex tasks" },
|
||||
{ value: "gpt-5.5-pro", label: "GPT-5.5 Pro — Flagship professional" },
|
||||
];
|
||||
|
||||
const GOOGLE_MODELS = [
|
||||
{ value: "gemini-2.5-flash", label: "Gemini 2.5 Flash — Fast & balanced (recommended)" },
|
||||
{ value: "gemini-2.5-pro", label: "Gemini 2.5 Pro — High capability, 1M context" },
|
||||
{ value: "gemini-3.1-flash-lite", label: "Gemini 3.1 Flash Lite — Optimized for speed/cost" },
|
||||
{ value: "gemini-3.5-flash", label: "Gemini 3.5 Flash — Latest frontier" },
|
||||
];
|
||||
|
||||
if (isLoading) {
|
||||
return <Card><CardContent className="py-6"><p className="text-sm text-gray-400">Loading...</p></CardContent></Card>;
|
||||
}
|
||||
@@ -214,6 +276,18 @@ export function AiSettingsCard() {
|
||||
<p className="text-sm text-gray-500">
|
||||
Enter your Google AI Studio API key to enable AI-powered SMS replies.
|
||||
</p>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">Model</label>
|
||||
<select
|
||||
value={settings?.googleAiModel ?? "gemini-2.5-flash"}
|
||||
onChange={(e) => providerModelMutation.mutate({ provider: "googleAi", model: e.target.value })}
|
||||
className="p-2 border rounded w-full text-sm bg-white"
|
||||
>
|
||||
{GOOGLE_MODELS.map((opt) => (
|
||||
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium">Google AI Studio API Key</label>
|
||||
<div className="relative mt-1">
|
||||
@@ -263,6 +337,9 @@ export function AiSettingsCard() {
|
||||
onToggle={(v) => providerToggleMutation.mutate({ provider: "openAi", enabled: v })}
|
||||
isSaving={providerKeyMutation.isPending && (providerKeyMutation.variables as any)?.provider === "openAi"}
|
||||
isToggling={providerToggleMutation.isPending && (providerToggleMutation.variables as any)?.provider === "openAi"}
|
||||
modelOptions={OPENAI_MODELS}
|
||||
selectedModel={settings?.openAiModel ?? "gpt-4o-mini"}
|
||||
onModelChange={(model) => providerModelMutation.mutate({ provider: "openAi", model })}
|
||||
/>
|
||||
|
||||
<Separator />
|
||||
@@ -278,6 +355,9 @@ export function AiSettingsCard() {
|
||||
onToggle={(v) => providerToggleMutation.mutate({ provider: "claudeAi", enabled: v })}
|
||||
isSaving={providerKeyMutation.isPending && (providerKeyMutation.variables as any)?.provider === "claudeAi"}
|
||||
isToggling={providerToggleMutation.isPending && (providerToggleMutation.variables as any)?.provider === "claudeAi"}
|
||||
modelOptions={CLAUDE_MODELS}
|
||||
selectedModel={settings?.claudeAiModel ?? "claude-haiku-4-5-20251001"}
|
||||
onModelChange={(model) => providerModelMutation.mutate({ provider: "claudeAi", model })}
|
||||
/>
|
||||
|
||||
<Separator />
|
||||
|
||||
Reference in New Issue
Block a user