From 7d897ad9bb5ee46839ec91992cbbf4593168f119 Mon Sep 17 00:00:00 2001
From: YurenHao0426 <blackhao0426@gmail.com>
Date: Fri, 13 Feb 2026 03:02:36 +0000
Subject: Add Claude provider, OpenRouter fallback, and GFM markdown support

- Add Claude (Anthropic) as third LLM provider with streaming support
- Add OpenRouter as transparent fallback when official API keys are missing or fail
- Add remark-gfm to ReactMarkdown for table/strikethrough rendering
- Claude models: sonnet-4.5, opus-4, opus-4.5, opus-4.6
- Backend: new stream_claude(), stream_openrouter(), provider routing, API key CRUD
- Frontend: model selectors, API key inputs for Claude and OpenRouter
- Auto-migration for new DB columns (claude_api_key, openrouter_api_key)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---
 frontend/src/components/Sidebar.tsx | 25 ++++++++++++++++++++-----
 1 file changed, 20 insertions(+), 5 deletions(-)

(limited to 'frontend/src/components/Sidebar.tsx')

diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx
index 5141081..78d2475 100644
--- a/frontend/src/components/Sidebar.tsx
+++ b/frontend/src/components/Sidebar.tsx
@@ -5,6 +5,7 @@ import { useAuthStore } from '../store/authStore';
 import type { NodeData, Trace, Message, MergedTrace, MergeStrategy } from '../store/flowStore';
 import type { Edge } from 'reactflow';
 import ReactMarkdown from 'react-markdown';
+import remarkGfm from 'remark-gfm';
 import { Play, Settings, Info, ChevronLeft, ChevronRight, Maximize2, Edit3, X, Check, FileText, MessageCircle, Send, GripVertical, GitMerge, Trash2, AlertCircle, Loader2, Navigation, Upload, Search, Link } from 'lucide-react';
 
 interface SidebarProps {
@@ -312,7 +313,7 @@ const Sidebar: React.FC<SidebarProps> = ({ isOpen, onToggle, onInteract }) => {
           scopes,
           merge_strategy: selectedNode.data.mergeStrategy || 'smart',
           config: {
-            provider: selectedNode.data.model.includes('gpt') || selectedNode.data.model === 'o3' ? 'openai' : 'google',
+            provider: selectedNode.data.model.includes('claude') ? 'claude' : (selectedNode.data.model.includes('gpt') || selectedNode.data.model === 'o3') ? 'openai' : 'google',
             model_name: selectedNode.data.model,
             temperature: selectedNode.data.temperature,
             system_prompt: selectedNode.data.systemPrompt,
@@ -1004,7 +1005,9 @@ const Sidebar: React.FC<SidebarProps> = ({ isOpen, onToggle, onInteract }) => {
 
     try {
       // Determine provider
+      const isClaude = modelAtSend.includes('claude');
       const isOpenAI = modelAtSend.includes('gpt') || modelAtSend === 'o3';
+      const provider = isClaude ? 'claude' : isOpenAI ? 'openai' : 'google';
       const reasoningModels = ['gpt-5', 'gpt-5-chat-latest', 'gpt-5-mini', 'gpt-5-nano', 'gpt-5-pro', 'gpt-5.1', 'gpt-5.1-chat-latest', 'gpt-5.2', 'gpt-5.2-chat-latest', 'gpt-5.2-pro', 'o3'];
       const isReasoning = reasoningModels.includes(modelAtSend);
 
@@ -1024,7 +1027,7 @@ const Sidebar: React.FC<SidebarProps> = ({ isOpen, onToggle, onInteract }) => {
           scopes,
           merge_strategy: 'smart',
           config: {
-            provider: isOpenAI ? 'openai' : 'google',
+            provider,
             model_name: modelAtSend,
             temperature: isReasoning ? 1 : tempAtSend,
             enable_google_search: webSearchAtSend,
@@ -1424,6 +1427,12 @@ const Sidebar: React.FC<SidebarProps> = ({ isOpen, onToggle, onInteract }) => {
                 }}
                 className="w-full border border-gray-300 rounded-md p-2 text-sm"
               >
+                <optgroup label="Claude">
+                  <option value="claude-sonnet-4-5">claude-sonnet-4.5</option>
+                  <option value="claude-opus-4">claude-opus-4</option>
+                  <option value="claude-opus-4-5">claude-opus-4.5</option>
+                  <option value="claude-opus-4-6">claude-opus-4.6</option>
+                </optgroup>
                 <optgroup label="Gemini">
                   <option value="gemini-2.5-flash">gemini-2.5-flash</option>
                   <option value="gemini-2.5-flash-lite">gemini-2.5-flash-lite</option>
@@ -1841,7 +1850,7 @@ const Sidebar: React.FC<SidebarProps> = ({ isOpen, onToggle, onInteract }) => {
                     ? 'bg-gray-900 border-gray-700 prose-invert text-gray-200' 
                     : 'bg-gray-50 border-gray-200 text-gray-900'
                 }`}>
-                  <ReactMarkdown>{selectedNode.data.response || (streamingNodeId === selectedNode.id ? streamBuffer : '')}</ReactMarkdown>
+                  <ReactMarkdown remarkPlugins={[remarkGfm]}>{selectedNode.data.response || (streamingNodeId === selectedNode.id ? streamBuffer : '')}</ReactMarkdown>
                 </div>
               )}
             </div>
@@ -2135,7 +2144,7 @@ const Sidebar: React.FC<SidebarProps> = ({ isOpen, onToggle, onInteract }) => {
                 />
               ) : (
                 <div className="prose prose-sm max-w-none">
-                  <ReactMarkdown>{selectedNode.data.response}</ReactMarkdown>
+                  <ReactMarkdown remarkPlugins={[remarkGfm]}>{selectedNode.data.response}</ReactMarkdown>
                 </div>
               )}
             </div>
@@ -2419,6 +2428,12 @@ const Sidebar: React.FC<SidebarProps> = ({ isOpen, onToggle, onInteract }) => {
                     isDark ? 'bg-gray-700 border-gray-600 text-gray-200' : 'border-gray-300 text-gray-900'
                   }`}
                 >
+                  <optgroup label="Claude">
+                    <option value="claude-sonnet-4-5">claude-sonnet-4.5</option>
+                    <option value="claude-opus-4">claude-opus-4</option>
+                    <option value="claude-opus-4-5">claude-opus-4.5</option>
+                    <option value="claude-opus-4-6">claude-opus-4.6</option>
+                  </optgroup>
                   <optgroup label="Gemini">
                     <option value="gemini-2.5-flash">gemini-2.5-flash</option>
                     <option value="gemini-2.5-flash-lite">gemini-2.5-flash-lite</option>
@@ -2520,7 +2535,7 @@ const Sidebar: React.FC<SidebarProps> = ({ isOpen, onToggle, onInteract }) => {
                             <p className="whitespace-pre-wrap">{msg.content}</p>
                           ) : (
                             <div className={`prose prose-sm max-w-none ${isDark ? 'prose-invert' : ''}`}>
-                              <ReactMarkdown>{msg.content}</ReactMarkdown>
+                              <ReactMarkdown remarkPlugins={[remarkGfm]}>{msg.content}</ReactMarkdown>
                             </div>
                           )}
                         </div>
-- 
cgit v1.2.3

