From 12ce53f301c9886b917a6b9cec81fb5e9fbfa1e3 Mon Sep 17 00:00:00 2001 From: Akshay Deo Date: Sun, 13 Jul 2025 06:46:52 +0530 Subject: [PATCH] makefile changes --- .gitignore | 11 +- .prettierrc | 18 + Makefile | 135 ++ transports/bifrost-http/.air.toml | 68 + ui/.prettierrc | 11 - ui/app/config/page.tsx | 128 +- ui/app/globals.css | 3 +- ui/app/layout.tsx | 72 +- ui/app/mcp-clients/page.tsx | 35 + ui/app/page.tsx | 507 ++++--- ui/app/plugins/page.tsx | 726 +++++----- ui/app/providers/page.tsx | 34 + ui/components/config/mcp-client-form.tsx | 28 +- ui/components/config/mcp-clients-lists.tsx | 332 ++--- ui/components/config/provider-form.tsx | 1425 ++++++++++---------- ui/components/config/providers-list.tsx | 34 +- ui/components/logs/empty-state.tsx | 386 +++--- ui/components/sidebar.tsx | 74 +- ui/components/ui/dialog.tsx | 7 +- 19 files changed, 2120 insertions(+), 1914 deletions(-) create mode 100644 .prettierrc create mode 100644 Makefile create mode 100644 transports/bifrost-http/.air.toml delete mode 100644 ui/.prettierrc create mode 100644 ui/app/mcp-clients/page.tsx create mode 100644 ui/app/providers/page.tsx diff --git a/.gitignore b/.gitignore index d17bbe3a3e..b712d55f96 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,13 @@ **/__pycache__/** private.* .venv -**/temp/ \ No newline at end of file + +# Temporary directories +**/temp/ +**/tmp/ +temp/ +tmp/ + +# Specific transport ignores +transports/bifrost-http/logs/ +transports/bifrost-http/tmp/ \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000000..2aa19cc4f9 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,18 @@ +{ + "root": true, + "printWidth": 140, + "singleQuote": true, + "semi": false, + "bracketSpacing": true, + "bracketSameLine": false, + "useTabs": false, + "tabWidth": 2, + "trailingComma": "all", + "plugins": [ + "prettier-plugin-tailwindcss" + ], + "tailwindFunctions": [ + "cn", + "classNames" + ] +} \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000000..c0ec26fe75 --- /dev/null +++ b/Makefile @@ -0,0 +1,135 @@ +# Makefile for Bifrost + +# Variables +CONFIG_FILE ?= transports/config.example.json +PORT ?= 8080 +POOL_SIZE ?= 300 +PLUGINS ?= maxim +PROMETHEUS_LABELS ?= + +# Colors for output +RED=\033[0;31m +GREEN=\033[0;32m +YELLOW=\033[1;33m +BLUE=\033[0;34m +CYAN=\033[0;36m +NC=\033[0m # No Color + +.PHONY: help dev dev-ui build run install-air clean test install-ui + +# Default target +help: ## Show this help message + @echo "$(BLUE)Bifrost Development - Available Commands:$(NC)" + @echo "" + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " $(GREEN)%-15s$(NC) %s\n", $$1, $$2}' + @echo "" + @echo "$(YELLOW)Environment Variables:$(NC)" + @echo " CONFIG_FILE Path to config file (default: transports/config.example.json)" + @echo " PORT Server port (default: 8080)" + @echo " POOL_SIZE Connection pool size (default: 300)" + @echo " PLUGINS Comma-separated plugins to load (default: maxim)" + @echo " PROMETHEUS_LABELS Labels for Prometheus metrics" + + +install-ui: + @which node > /dev/null || (echo "$(RED)Error: Node.js is not installed. Please install Node.js first.$(NC)" && exit 1) + @which npm > /dev/null || (echo "$(RED)Error: npm is not installed. Please install npm first.$(NC)" && exit 1) + @echo "$(GREEN)Node.js and npm are installed$(NC)" + @cd ui && npm install + @which next > /dev/null || (echo "$(YELLOW)Installing nextjs...$(NC)" && npm install -g next) + @echo "$(GREEN)UI deps are in sync$(NC)" + +install-air: ## Install air for hot reloading (if not already installed) + @which air > /dev/null || (echo "$(YELLOW)Installing air for hot reloading...$(NC)" && go install github.com/air-verse/air@latest) + @echo "$(GREEN)Air is ready$(NC)" + +dev-http: install-ui install-air ## Start complete development environment (UI + API with proxy) + @echo "$(GREEN)Starting Bifrost complete development environment...$(NC)" + @echo "$(YELLOW)This will start:$(NC)" + @echo " 1. UI development server (localhost:3000)" + @echo " 2. API server with UI proxy (localhost:$(PORT)/ui)" + @echo "$(CYAN)Access everything at: http://localhost:$(PORT)/ui$(NC)" + @echo "" + @echo "$(YELLOW)Starting UI development server...$(NC)" + @cd ui && npm run dev & + @sleep 3 + @echo "$(YELLOW)Starting API server with UI proxy...$(NC)" + @cd transports/bifrost-http && BIFROST_UI_DEV=true air -c .air.toml -- \ + -port "$(PORT)" \ + -plugins "$(PLUGINS)" \ + $(if $(PROMETHEUS_LABELS),-prometheus-labels "$(PROMETHEUS_LABELS)") + +build: ## Build bifrost-http binary + @echo "$(GREEN)Building bifrost-http...$(NC)" + @cd transports/bifrost-http && go build -o ../../tmp/bifrost-http . + @echo "$(GREEN)Built: tmp/bifrost-http$(NC)" + +run: build ## Build and run bifrost-http (no hot reload) + @echo "$(GREEN)Running bifrost-http...$(NC)" + @./tmp/bifrost-http \ + -config "$(CONFIG_FILE)" \ + -port "$(PORT)" \ + -pool-size $(POOL_SIZE) \ + -plugins "$(PLUGINS)" \ + $(if $(PROMETHEUS_LABELS),-prometheus-labels "$(PROMETHEUS_LABELS)") + + +clean: ## Clean build artifacts and temporary files + @echo "$(YELLOW)Cleaning build artifacts...$(NC)" + @rm -rf tmp/ + @rm -f transports/bifrost-http/build-errors.log + @rm -rf transports/bifrost-http/tmp/ + @echo "$(GREEN)Clean complete$(NC)" + +test: ## Run tests for bifrost-http + @echo "$(GREEN)Running bifrost-http tests...$(NC)" + @cd transports/bifrost-http && go test -v ./... + +test-core: ## Run core tests + @echo "$(GREEN)Running core tests...$(NC)" + @cd core && go test -v ./... + +test-plugins: ## Run plugin tests + @echo "$(GREEN)Running plugin tests...$(NC)" + @cd plugins && find . -name "*.go" -path "*/tests/*" -o -name "*_test.go" | head -1 > /dev/null && \ + for dir in $$(find . -name "*_test.go" -exec dirname {} \; | sort -u); do \ + echo "Testing $$dir..."; \ + cd $$dir && go test -v ./... && cd - > /dev/null; \ + done || echo "No plugin tests found" + +test-all: test-core test-plugins test ## Run all tests + +# Quick start with example config +quick-start: ## Quick start with example config and maxim plugin + @echo "$(GREEN)Quick starting Bifrost with example configuration...$(NC)" + @$(MAKE) dev CONFIG_FILE=transports/config.example.json PLUGINS=maxim + +# Docker targets +docker-build: ## Build Docker image + @echo "$(GREEN)Building Docker image...$(NC)" + @cd transports && docker build -t bifrost . + @echo "$(GREEN)Docker image built: bifrost$(NC)" + +docker-run: ## Run Docker container + @echo "$(GREEN)Running Docker container...$(NC)" + @docker run -p $(PORT):$(PORT) \ + -v $(PWD)/$(CONFIG_FILE):/app/config/config.json \ + --env-file <(env | grep -E '^(OPENAI|ANTHROPIC|AZURE|AWS|COHERE|VERTEX)_') \ + bifrost + +# Linting and formatting +lint: ## Run linter for Go code + @echo "$(GREEN)Running golangci-lint...$(NC)" + @golangci-lint run ./... + +fmt: ## Format Go code + @echo "$(GREEN)Formatting Go code...$(NC)" + @gofmt -s -w . + @goimports -w . + +# Git hooks and development setup +setup-git-hooks: ## Set up Git hooks for development + @echo "$(GREEN)Setting up Git hooks...$(NC)" + @echo "#!/bin/sh\nmake fmt\nmake lint" > .git/hooks/pre-commit + @chmod +x .git/hooks/pre-commit + @echo "$(GREEN)Git hooks installed$(NC)" \ No newline at end of file diff --git a/transports/bifrost-http/.air.toml b/transports/bifrost-http/.air.toml new file mode 100644 index 0000000000..888a68ab1c --- /dev/null +++ b/transports/bifrost-http/.air.toml @@ -0,0 +1,68 @@ +root = "." +testdata_dir = "testdata" +tmp_dir = "./tmp" + +[build] + args_bin = [] + bin = "./tmp/main" + cmd = "go build -o ./tmp/main ." + delay = 1000 + exclude_dir = ["assets", "tmp", "vendor", "testdata", "ui", "node_modules"] + exclude_file = [] + exclude_regex = ["_test.go"] + exclude_unchanged = false + follow_symlink = false + full_bin = "" + include_dir = [] + include_ext = ["go", "tpl", "tmpl", "html"] + include_file = [] + kill_delay = "0s" + log = "build-errors.log" + poll = false + poll_interval = 0 + rerun = false + rerun_delay = 500 + send_interrupt = false + stop_on_root = false + +[color] + app = "" + build = "yellow" + main = "magenta" + runner = "green" + watcher = "cyan" + +[log] + main_only = false + time = false + +[misc] + clean_on_exit = false + +[proxy] + enabled = false + proxy_port = 8090 + app_port = 8080 + +[screen] + clear_on_rebuild = false + keep_scroll = true + +# Watch directories +[[build.watch_dirs]] + dir = "." + +[[build.watch_dirs]] + dir = "../../core" + +[[build.watch_dirs]] + dir = "./handlers" + +[[build.watch_dirs]] + dir = "./integrations" + +[[build.watch_dirs]] + dir = "./lib" + +[[build.watch_dirs]] + dir = "./plugins" diff --git a/ui/.prettierrc b/ui/.prettierrc deleted file mode 100644 index 1e38279b43..0000000000 --- a/ui/.prettierrc +++ /dev/null @@ -1,11 +0,0 @@ -{ - "printWidth": 140, - "singleQuote": false, - "bracketSpacing": true, - "jsxBracketSameLine": false, - "useTabs": true, - "tabWidth": 2, - "trailingComma": "all", - "plugins": ["prettier-plugin-tailwindcss"], - "tailwindFunctions": ["cn", "classNames"] -} diff --git a/ui/app/config/page.tsx b/ui/app/config/page.tsx index 41da047583..f34cf355dc 100644 --- a/ui/app/config/page.tsx +++ b/ui/app/config/page.tsx @@ -1,119 +1,17 @@ -"use client"; +'use client' -import { useState, useEffect } from "react"; -import Header from "@/components/header"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { Badge } from "@/components/ui/badge"; -import { Settings, Database, Zap } from "lucide-react"; -import { useToast } from "@/hooks/use-toast"; -import { ProviderResponse } from "@/lib/types/config"; -import { apiService } from "@/lib/api"; -import CoreSettingsList from "@/components/config/core-settings-list"; -import ProvidersList from "@/components/config/providers-list"; -import MCPClientsList from "@/components/config/mcp-clients-lists"; -import { MCPClient } from "@/lib/types/mcp"; -import FullPageLoader from "@/components/full-page-loader"; +import CoreSettingsList from '@/components/config/core-settings-list' export default function ConfigPage() { - const [activeTab, setActiveTab] = useState("providers"); - const [isLoadingProviders, setIsLoadingProviders] = useState(true); - const [isLoadingMcpClients, setIsLoadingMcpClients] = useState(true); - const [providers, setProviders] = useState([]); - const [mcpClients, setMcpClients] = useState([]); - - const { toast } = useToast(); - - // Load configuration data - useEffect(() => { - loadProviders(); - loadMcpClients(); - }, []); - - const loadProviders = async () => { - const [data, error] = await apiService.getProviders(); - setIsLoadingProviders(false); - - if (error) { - toast({ - title: "Error", - description: error, - variant: "destructive", - }); - return; - } - setProviders(data?.providers || []); - }; - - const loadMcpClients = async () => { - const [data, error] = await apiService.getMCPClients(); - setIsLoadingMcpClients(false); - - if (error) { - toast({ - title: "Error", - description: error, - variant: "destructive", - }); - return; - } - - setMcpClients(data || []); - }; - - return ( -
- {isLoadingProviders || isLoadingMcpClients ? ( - - ) : ( -
- {/* Page Header */} -
-

Configuration

-

Configure AI providers, API keys, and system settings for your Bifrost instance.

-
- - {/* Configuration Tabs */} - - - - - Providers - - {providers.length} - - - - - MCP Clients - {mcpClients.length > 0 && ( - - {mcpClients.length} - - )} - - - - Core Settings - - - - {/* Providers Tab */} - - - - - {/* MCP Tools Tab */} - - - - - {/* Core Settings Tab */} - - - - -
- )} -
- ); + return ( +
+ {/* Page Header */} +
+

Configuration

+

Configure AI providers, API keys, and system settings for your Bifrost instance.

+
+ + +
+ ) } diff --git a/ui/app/globals.css b/ui/app/globals.css index cd2c7836c6..c7d7740f8b 100644 --- a/ui/app/globals.css +++ b/ui/app/globals.css @@ -42,6 +42,7 @@ --radius-lg: var(--radius); --radius-xl: calc(var(--radius) + 4px); --height-base: calc(100vh - 6rem); + } :root { @@ -52,7 +53,7 @@ --card-foreground: oklch(0.141 0.005 285.823); --popover: oklch(1 0 0); --popover-foreground: oklch(0.141 0.005 285.823); - --primary: oklch(0.21 0.006 285.885); + --primary: oklch(0.5081 0.1049 165.61); --primary-foreground: oklch(0.985 0 0); --secondary: oklch(0.967 0.001 286.375); --secondary-foreground: oklch(0.21 0.006 285.885); diff --git a/ui/app/layout.tsx b/ui/app/layout.tsx index 9215fe40fe..f7f2386e5c 100644 --- a/ui/app/layout.tsx +++ b/ui/app/layout.tsx @@ -1,45 +1,45 @@ -import type { Metadata } from "next"; -import { Geist, Geist_Mono } from "next/font/google"; -import "./globals.css"; -import Sidebar from "@/components/sidebar"; -import { SidebarProvider } from "@/components/ui/sidebar"; -import { ThemeProvider } from "@/components/theme-provider"; -import { Toaster } from "sonner"; -import ProgressProvider from "@/components/progress-bar"; -import { WebSocketProvider } from "@/hooks/useWebSocket"; +import ProgressProvider from '@/components/progress-bar' +import Sidebar from '@/components/sidebar' +import { ThemeProvider } from '@/components/theme-provider' +import { SidebarProvider } from '@/components/ui/sidebar' +import { WebSocketProvider } from '@/hooks/useWebSocket' +import type { Metadata } from 'next' +import { Geist, Geist_Mono } from 'next/font/google' +import { Toaster } from 'sonner' +import './globals.css' const geistSans = Geist({ - variable: "--font-geist-sans", - subsets: ["latin"], -}); + variable: '--font-geist-sans', + subsets: ['latin'], +}) const geistMono = Geist_Mono({ - variable: "--font-geist-mono", - subsets: ["latin"], -}); + variable: '--font-geist-mono', + subsets: ['latin'], +}) export const metadata: Metadata = { - title: "Bifrost - The fastest LLM gateway", - description: - "Production-ready fastest LLM gateway that connects to 8+ providers through a single API. Get automatic failover, load balancing, mcp support and zero-downtime deployments.", -}; + title: 'Bifrost - The fastest LLM gateway', + description: + 'Production-ready fastest LLM gateway that connects to 8+ providers through a single API. Get automatic failover, load balancing, mcp support and zero-downtime deployments.', +} export default function RootLayout({ children }: { children: React.ReactNode }) { - return ( - - - - - - - - -
{children}
-
-
-
-
- - - ); + return ( + + + + + + + + +
{children}
+
+
+
+
+ + + ) } diff --git a/ui/app/mcp-clients/page.tsx b/ui/app/mcp-clients/page.tsx new file mode 100644 index 0000000000..9adb596cb9 --- /dev/null +++ b/ui/app/mcp-clients/page.tsx @@ -0,0 +1,35 @@ +'use client' + +import MCPClientsList from '@/components/config/mcp-clients-lists' +import FullPageLoader from '@/components/full-page-loader' +import { useToast } from '@/hooks/use-toast' +import { apiService } from '@/lib/api' +import { MCPClient } from '@/lib/types/mcp' +import { useEffect, useState } from 'react' +export default function MCPServersPage() { + const [mcpClients, setMcpClients] = useState([]) + const [isLoadingMcpClients, setIsLoadingMcpClients] = useState(true) + const { toast } = useToast() + + useEffect(() => { + loadMcpClients() + }, []) + + const loadMcpClients = async () => { + const [data, error] = await apiService.getMCPClients() + setIsLoadingMcpClients(false) + + if (error) { + toast({ + title: 'Error', + description: error, + variant: 'destructive', + }) + return + } + + setMcpClients(data || []) + } + + return
{isLoadingMcpClients ? : }
+} diff --git a/ui/app/page.tsx b/ui/app/page.tsx index 783cac45b5..3a37a21277 100644 --- a/ui/app/page.tsx +++ b/ui/app/page.tsx @@ -1,287 +1,286 @@ -"use client"; +'use client' -import { useState, useEffect, useCallback, useMemo } from "react"; -import Header from "@/components/header"; -import { LogsDataTable } from "@/components/logs/logs-table"; -import { LogDetailSheet } from "@/components/logs/log-detail-sheet"; -import { createColumns } from "@/components/logs/columns"; -import type { LogEntry, LogFilters, Pagination, BifrostMessage, MessageContent, ContentBlock, LogStats } from "@/lib/types/logs"; -import { apiService } from "@/lib/api"; -import { Card, CardContent } from "@/components/ui/card"; -import { Alert, AlertDescription } from "@/components/ui/alert"; -import { AlertCircle, BarChart, CheckCircle, Clock, Hash } from "lucide-react"; -import { useWebSocket } from "@/hooks/useWebSocket"; -import { EmptyState } from "@/components/logs/empty-state"; -import FullPageLoader from "@/components/full-page-loader"; +import FullPageLoader from '@/components/full-page-loader' +import { createColumns } from '@/components/logs/columns' +import { EmptyState } from '@/components/logs/empty-state' +import { LogDetailSheet } from '@/components/logs/log-detail-sheet' +import { LogsDataTable } from '@/components/logs/logs-table' +import { Alert, AlertDescription } from '@/components/ui/alert' +import { Card, CardContent } from '@/components/ui/card' +import { useWebSocket } from '@/hooks/useWebSocket' +import { apiService } from '@/lib/api' +import type { BifrostMessage, ContentBlock, LogEntry, LogFilters, LogStats, MessageContent, Pagination } from '@/lib/types/logs' +import { AlertCircle, BarChart, CheckCircle, Clock, Hash } from 'lucide-react' +import { useCallback, useEffect, useMemo, useState } from 'react' export default function LogsPage() { - const [logs, setLogs] = useState([]); - const [totalItems, setTotalItems] = useState(0); // changes with filters - const [stats, setStats] = useState(null); - const [initialLoading, setInitialLoading] = useState(true); // on initial load - const [fetchingLogs, setFetchingLogs] = useState(false); // on pagination/filters change - const [error, setError] = useState(null); - const [showEmptyState, setShowEmptyState] = useState(false); + const [logs, setLogs] = useState([]) + const [totalItems, setTotalItems] = useState(0) // changes with filters + const [stats, setStats] = useState(null) + const [initialLoading, setInitialLoading] = useState(true) // on initial load + const [fetchingLogs, setFetchingLogs] = useState(false) // on pagination/filters change + const [error, setError] = useState(null) + const [showEmptyState, setShowEmptyState] = useState(false) - const [selectedLog, setSelectedLog] = useState(null); + const [selectedLog, setSelectedLog] = useState(null) - const [filters, setFilters] = useState({ - providers: [], - models: [], - status: [], - content_search: "", - }); - const [pagination, setPagination] = useState({ - limit: 50, - offset: 0, - sort_by: "timestamp", - order: "desc", - }); + const [filters, setFilters] = useState({ + providers: [], + models: [], + status: [], + content_search: '', + }) + const [pagination, setPagination] = useState({ + limit: 50, + offset: 0, + sort_by: 'timestamp', + order: 'desc', + }) - const handleNewLog = useCallback( - (log: LogEntry) => { - // If we were in empty state, exit it since we now have logs - if (showEmptyState) { - setShowEmptyState(false); - } + const handleNewLog = useCallback( + (log: LogEntry) => { + // If we were in empty state, exit it since we now have logs + if (showEmptyState) { + setShowEmptyState(false) + } - // Only prepend the new log if we're on the first page and sorted by timestamp desc - if (pagination.offset === 0 && pagination.sort_by === "timestamp" && pagination.order === "desc") { - // Check if the log matches current filters - if (!matchesFilters(log, filters)) { - return; - } + // Only prepend the new log if we're on the first page and sorted by timestamp desc + if (pagination.offset === 0 && pagination.sort_by === 'timestamp' && pagination.order === 'desc') { + // Check if the log matches current filters + if (!matchesFilters(log, filters)) { + return + } - setLogs((prevLogs: LogEntry[]) => { - // Remove the last log if we're at the page limit - const updatedLogs = [log, ...prevLogs]; - if (updatedLogs.length > pagination.limit) { - updatedLogs.pop(); - } - return updatedLogs; - }); - setTotalItems((prev: number) => prev + 1); + setLogs((prevLogs: LogEntry[]) => { + // Remove the last log if we're at the page limit + const updatedLogs = [log, ...prevLogs] + if (updatedLogs.length > pagination.limit) { + updatedLogs.pop() + } + return updatedLogs + }) + setTotalItems((prev: number) => prev + 1) - setStats((prevStats) => { - if (!prevStats) return prevStats; + setStats((prevStats) => { + if (!prevStats) return prevStats - const newStats = { ...prevStats }; - newStats.total_requests += 1; + const newStats = { ...prevStats } + newStats.total_requests += 1 - // Update success rate - const successCount = (prevStats.success_rate / 100) * prevStats.total_requests; - const newSuccessCount = log.status === "success" ? successCount + 1 : successCount; - newStats.success_rate = (newSuccessCount / newStats.total_requests) * 100; + // Update success rate + const successCount = (prevStats.success_rate / 100) * prevStats.total_requests + const newSuccessCount = log.status === 'success' ? successCount + 1 : successCount + newStats.success_rate = (newSuccessCount / newStats.total_requests) * 100 - // Update average latency - if (log.latency) { - const totalLatency = prevStats.average_latency * prevStats.total_requests; - newStats.average_latency = (totalLatency + log.latency) / newStats.total_requests; - } + // Update average latency + if (log.latency) { + const totalLatency = prevStats.average_latency * prevStats.total_requests + newStats.average_latency = (totalLatency + log.latency) / newStats.total_requests + } - // Update total tokens - if (log.token_usage) { - newStats.total_tokens += log.token_usage.total_tokens; - } + // Update total tokens + if (log.token_usage) { + newStats.total_tokens += log.token_usage.total_tokens + } - return newStats; - }); - } - }, - [pagination.offset, pagination.sort_by, pagination.order, pagination.limit, filters, showEmptyState], - ); + return newStats + }) + } + }, + [pagination.offset, pagination.sort_by, pagination.order, pagination.limit, filters, showEmptyState], + ) - const { isConnected: isSocketConnected, setMessageHandler } = useWebSocket(); + const { isConnected: isSocketConnected, setMessageHandler } = useWebSocket() - // Set up the message handler when the component mounts - useEffect(() => { - setMessageHandler(handleNewLog); - }, [handleNewLog, setMessageHandler]); + // Set up the message handler when the component mounts + useEffect(() => { + setMessageHandler(handleNewLog) + }, [handleNewLog, setMessageHandler]) - const fetchLogs = useCallback(async () => { - setFetchingLogs(true); - setError(null); + const fetchLogs = useCallback(async () => { + setFetchingLogs(true) + setError(null) - try { - const [response, err] = await apiService.getLogs(filters, pagination); + try { + const [response, err] = await apiService.getLogs(filters, pagination) - if (err) { - setError(err); - setLogs([]); - setTotalItems(0); - } else if (response) { - setLogs(response.logs || []); - setTotalItems(response.stats.total_requests); - setStats(response.stats); - } + if (err) { + setError(err) + setLogs([]) + setTotalItems(0) + } else if (response) { + setLogs(response.logs || []) + setTotalItems(response.stats.total_requests) + setStats(response.stats) + } - // Only set showEmptyState on initial load and only based on total logs - if (initialLoading) { - // Check if there are any logs globally, not just in the current filter - setShowEmptyState(response ? response.stats.total_requests === 0 : true); - } - } catch { - setError("Cannot fetch logs. Please check if logs are enabled in your Bifrost config."); - setLogs([]); - setTotalItems(0); - setShowEmptyState(true); - } finally { - setFetchingLogs(false); - } - }, [filters, pagination, initialLoading]); + // Only set showEmptyState on initial load and only based on total logs + if (initialLoading) { + // Check if there are any logs globally, not just in the current filter + setShowEmptyState(response ? response.stats.total_requests === 0 : true) + } + } catch { + setError('Cannot fetch logs. Please check if logs are enabled in your Bifrost config.') + setLogs([]) + setTotalItems(0) + setShowEmptyState(true) + } finally { + setFetchingLogs(false) + } + }, [filters, pagination, initialLoading]) - // Fetch logs when filters or pagination change - useEffect(() => { - if (!initialLoading) fetchLogs(); - }, [fetchLogs, initialLoading]); + // Fetch logs when filters or pagination change + useEffect(() => { + if (!initialLoading) fetchLogs() + }, [fetchLogs, initialLoading]) - useEffect(() => { - fetchLogs(); - setInitialLoading(false); - }, []); + useEffect(() => { + fetchLogs() + setInitialLoading(false) + }, []) - const getMessageText = (content: MessageContent): string => { - if (typeof content === "string") { - return content; - } - if (Array.isArray(content)) { - return content.reduce((acc: string, block: ContentBlock) => { - if (block.type === "text" && block.text) { - return acc + block.text; - } - return acc; - }, ""); - } - return ""; - }; + const getMessageText = (content: MessageContent): string => { + if (typeof content === 'string') { + return content + } + if (Array.isArray(content)) { + return content.reduce((acc: string, block: ContentBlock) => { + if (block.type === 'text' && block.text) { + return acc + block.text + } + return acc + }, '') + } + return '' + } - // Helper function to check if a log matches the current filters - const matchesFilters = (log: LogEntry, filters: LogFilters): boolean => { - if (filters.providers?.length && !filters.providers.includes(log.provider)) { - return false; - } - if (filters.models?.length && !filters.models.includes(log.model)) { - return false; - } - if (filters.status?.length && !filters.status.includes(log.status)) { - return false; - } - if (filters.start_time && new Date(log.timestamp) < new Date(filters.start_time)) { - return false; - } - if (filters.end_time && new Date(log.timestamp) > new Date(filters.end_time)) { - return false; - } - if (filters.min_latency && (!log.latency || log.latency < filters.min_latency)) { - return false; - } - if (filters.max_latency && (!log.latency || log.latency > filters.max_latency)) { - return false; - } - if (filters.min_tokens && (!log.token_usage || log.token_usage.total_tokens < filters.min_tokens)) { - return false; - } - if (filters.max_tokens && (!log.token_usage || log.token_usage.total_tokens > filters.max_tokens)) { - return false; - } - if (filters.content_search) { - const search = filters.content_search.toLowerCase(); - const content = [ - ...(log.input_history || []).map((msg: BifrostMessage) => getMessageText(msg.content)), - log.output_message ? getMessageText(log.output_message.content) : "", - ] - .join(" ") - .toLowerCase(); + // Helper function to check if a log matches the current filters + const matchesFilters = (log: LogEntry, filters: LogFilters): boolean => { + if (filters.providers?.length && !filters.providers.includes(log.provider)) { + return false + } + if (filters.models?.length && !filters.models.includes(log.model)) { + return false + } + if (filters.status?.length && !filters.status.includes(log.status)) { + return false + } + if (filters.start_time && new Date(log.timestamp) < new Date(filters.start_time)) { + return false + } + if (filters.end_time && new Date(log.timestamp) > new Date(filters.end_time)) { + return false + } + if (filters.min_latency && (!log.latency || log.latency < filters.min_latency)) { + return false + } + if (filters.max_latency && (!log.latency || log.latency > filters.max_latency)) { + return false + } + if (filters.min_tokens && (!log.token_usage || log.token_usage.total_tokens < filters.min_tokens)) { + return false + } + if (filters.max_tokens && (!log.token_usage || log.token_usage.total_tokens > filters.max_tokens)) { + return false + } + if (filters.content_search) { + const search = filters.content_search.toLowerCase() + const content = [ + ...(log.input_history || []).map((msg: BifrostMessage) => getMessageText(msg.content)), + log.output_message ? getMessageText(log.output_message.content) : '', + ] + .join(' ') + .toLowerCase() - if (!content.includes(search)) { - return false; - } - } - return true; - }; + if (!content.includes(search)) { + return false + } + } + return true + } - const statCards = useMemo( - () => [ - { - title: "Total Requests", - value: stats?.total_requests.toLocaleString() || "-", - icon: , - }, - { - title: "Success Rate", - value: stats ? `${stats.success_rate.toFixed(2)}%` : "-", - icon: , - }, - { - title: "Avg Latency", - value: stats ? `${stats.average_latency.toFixed(2)}ms` : "-", - icon: , - }, - { - title: "Total Tokens", - value: stats?.total_tokens.toLocaleString() || "-", - icon: , - }, - ], - [stats], - ); + const statCards = useMemo( + () => [ + { + title: 'Total Requests', + value: stats?.total_requests.toLocaleString() || '-', + icon: , + }, + { + title: 'Success Rate', + value: stats ? `${stats.success_rate.toFixed(2)}%` : '-', + icon: , + }, + { + title: 'Avg Latency', + value: stats ? `${stats.average_latency.toFixed(2)}ms` : '-', + icon: , + }, + { + title: 'Total Tokens', + value: stats?.total_tokens.toLocaleString() || '-', + icon: , + }, + ], + [stats], + ) - const columns = createColumns(); + const columns = createColumns() - return ( -
- {initialLoading ? ( - - ) : showEmptyState ? ( - - ) : ( -
-
-

Request Logs

-

Monitor and analyze all API requests and responses

-
+ return ( +
+ {initialLoading ? ( + + ) : showEmptyState ? ( + + ) : ( +
+
+

Request Logs

+

Monitor and analyze all API requests and responses

+
-
- {/* Quick Stats */} -
- {statCards.map((card) => ( - - -
-
{card.title}
-
{card.value}
-
-
-
- ))} -
+
+ {/* Quick Stats */} +
+ {statCards.map((card) => ( + + +
+
{card.title}
+
{card.value}
+
+
+
+ ))} +
- {/* Error Alert */} - {error && ( - - - {error} - - )} + {/* Error Alert */} + {error && ( + + + {error} + + )} - -
+ +
- {/* Log Detail Sheet */} - !open && setSelectedLog(null)} /> -
- )} -
- ); + {/* Log Detail Sheet */} + !open && setSelectedLog(null)} /> +
+ )} +
+ ) } diff --git a/ui/app/plugins/page.tsx b/ui/app/plugins/page.tsx index 7627108709..8c9a6e38fa 100644 --- a/ui/app/plugins/page.tsx +++ b/ui/app/plugins/page.tsx @@ -1,392 +1,394 @@ -"use client"; +'use client' -import Header from "@/components/header"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; -import { Button } from "@/components/ui/button"; -import { Badge } from "@/components/ui/badge"; -import { Alert, AlertDescription } from "@/components/ui/alert"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { Puzzle, Code, Terminal, Rocket, Zap, Shield, Monitor, Database, Container, ChevronRight, AlertTriangle, Info } from "lucide-react"; -import Link from "next/link"; -import GradientHeader from "@/components/ui/gradient-header"; -import Image from "next/image"; -import { useTheme } from "next-themes"; -import { GithubLogoIcon } from "@phosphor-icons/react"; +import { Alert, AlertDescription } from '@/components/ui/alert' +import { Badge } from '@/components/ui/badge' +import { Button } from '@/components/ui/button' +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import GradientHeader from '@/components/ui/gradient-header' +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' +import { GithubLogoIcon } from '@phosphor-icons/react' +import { AlertTriangle, ChevronRight, Code, Container, Database, Info, Monitor, Puzzle, Rocket, Shield, Terminal, Zap } from 'lucide-react' +import { useTheme } from 'next-themes' +import Image from 'next/image' +import Link from 'next/link' const featuredPlugins = [ - { - name: "maxim", - displayName: "Maxim Logger", - description: "Advanced LLM observability, tracing, and analytics platform integration", - category: "Observability", - status: "production", - httpSupport: true, - capabilities: ["Real-time LLM tracing", "Performance analytics", "Cost tracking", "Error monitoring", "Custom session tracking"], - icon: Monitor, - color: "bg-blue-500", - url: "https://github.com/maximhq/bifrost/tree/main/plugins/maxim", - quickStart: { - http: "bifrost-http --plugins maxim", - docker: "docker run -e APP_PLUGINS=maxim bifrost-transport", - }, - }, - { - name: "mocker", - displayName: "Response Mocker", - description: "Mock AI responses for testing, development, and cost-effective prototyping", - category: "Development", - status: "production", - httpSupport: false, - capabilities: [ - "Configurable mock responses", - "Request pattern matching", - "Development environment support", - "Cost-free testing", - "Latency simulation", - ], - icon: Code, - color: "bg-blue-500", - url: "https://github.com/maximhq/bifrost/tree/main/plugins/mocker", - quickStart: { - http: "HTTP support coming soon", - docker: "HTTP support coming soon", - }, - }, - { - name: "circuit-breaker", - displayName: "Circuit Breaker", - description: "Resilience patterns for handling provider failures and preventing cascade errors", - category: "Reliability", - status: "enterprise", - httpSupport: false, - capabilities: ["Automatic failure detection", "Fallback mechanisms", "Rate limiting", "Health monitoring", "Recovery strategies"], - icon: Shield, - color: "bg-orange-500", - url: "https://github.com/maximhq/bifrost/tree/main/plugins/circuitbreaker", - quickStart: { - http: "HTTP support coming soon", - docker: "HTTP support coming soon", - }, - }, -]; + { + name: 'maxim', + displayName: 'Maxim Logger', + description: 'Advanced LLM observability, tracing, and analytics platform integration', + category: 'Observability', + status: 'production', + httpSupport: true, + capabilities: ['Real-time LLM tracing', 'Performance analytics', 'Cost tracking', 'Error monitoring', 'Custom session tracking'], + icon: Monitor, + color: 'bg-blue-500', + url: 'https://github.com/maximhq/bifrost/tree/main/plugins/maxim', + quickStart: { + http: 'bifrost-http --plugins maxim', + docker: 'docker run -e APP_PLUGINS=maxim bifrost-transport', + }, + }, + { + name: 'mocker', + displayName: 'Response Mocker', + description: 'Mock AI responses for testing, development, and cost-effective prototyping', + category: 'Development', + status: 'production', + httpSupport: false, + capabilities: [ + 'Configurable mock responses', + 'Request pattern matching', + 'Development environment support', + 'Cost-free testing', + 'Latency simulation', + ], + icon: Code, + color: 'bg-blue-500', + url: 'https://github.com/maximhq/bifrost/tree/main/plugins/mocker', + quickStart: { + http: 'HTTP support coming soon', + docker: 'HTTP support coming soon', + }, + }, + { + name: 'circuit-breaker', + displayName: 'Circuit Breaker', + description: 'Resilience patterns for handling provider failures and preventing cascade errors', + category: 'Reliability', + status: 'enterprise', + httpSupport: false, + capabilities: ['Automatic failure detection', 'Fallback mechanisms', 'Rate limiting', 'Health monitoring', 'Recovery strategies'], + icon: Shield, + color: 'bg-orange-500', + url: 'https://github.com/maximhq/bifrost/tree/main/plugins/circuitbreaker', + quickStart: { + http: 'HTTP support coming soon', + docker: 'HTTP support coming soon', + }, + }, +] const upcomingPlugins = [ - { - name: "Redis Cache", - description: "High-performance caching layer with Redis backend", - icon: Database, - status: "coming-soon", - }, - { - name: "Auth Guard", - description: "Enterprise authentication and authorization middleware", - icon: Shield, - status: "coming-soon", - }, - { - name: "Rate Limiter", - description: "Advanced rate limiting with multiple strategies", - icon: Zap, - status: "coming-soon", - }, -]; + { + name: 'Redis Cache', + description: 'High-performance caching layer with Redis backend', + icon: Database, + status: 'coming-soon', + }, + { + name: 'Auth Guard', + description: 'Enterprise authentication and authorization middleware', + icon: Shield, + status: 'coming-soon', + }, + { + name: 'Rate Limiter', + description: 'Advanced rate limiting with multiple strategies', + icon: Zap, + status: 'coming-soon', + }, +] export default function PluginsPage() { - const { resolvedTheme } = useTheme(); - return ( -
-
-
- {/* Hero Section */} -
-
- - Plugin Ecosystem - - Beta - -
+ const { resolvedTheme } = useTheme() + return ( +
+
+
+ {/* Hero Section */} +
+
+ + Plugin Ecosystem + + Beta + +
- + -

- Extend Bifrost with powerful plugins for observability, testing, security, and custom business logic. Full support in Go SDK, - with HTTP transport integration in active development. -

+

+ Extend Bifrost with powerful plugins for observability, testing, security, and custom business logic. Full support in Go SDK, + with HTTP transport integration in active development. +

-
- - -
-
+
+ + +
+
- {/* HTTP Transport Status */} - - - - HTTP transport support for custom and third party plugins is currently in active development and will be available soon. - - + {/* HTTP Transport Status */} + + + + HTTP transport support for custom and third party plugins is currently in active development and will be available soon. + + - {/* Featured Plugins */} -
-
-

Featured Plugins

-

Production-ready plugins with varying levels of HTTP transport support

-
+ {/* Featured Plugins */} +
+
+

Featured Plugins

+

Production-ready plugins with varying levels of HTTP transport support

+
-
- {featuredPlugins.map((plugin) => { - const Icon = plugin.icon; - return ( - - -
- {plugin.name == "maxim" ? ( - Maxim - ) : ( -
- -
- )} +
+ {featuredPlugins.map((plugin) => { + const Icon = plugin.icon + return ( + + +
+ {plugin.name == 'maxim' ? ( + Maxim + ) : ( +
+ +
+ )} - - {plugin.status} - -
+ + {plugin.status} + +
-
- {plugin.displayName} - - {plugin.category} - -
+
+ {plugin.displayName} + + {plugin.category} + +
- {plugin.description} - + {plugin.description} + - -
-
-

Key Features

-
- {plugin.capabilities.slice(0, 3).map((capability) => ( -
- - {capability} -
- ))} -
-
+ +
+
+

Key Features

+
+ {plugin.capabilities.slice(0, 3).map((capability) => ( +
+ + {capability} +
+ ))} +
+
- {plugin.httpSupport ? ( - - - - HTTP - - - Docker - - + {plugin.httpSupport ? ( + + + + HTTP + + + Docker + + - -
-
- - Command Line -
- {plugin.quickStart.http} -
-
+ +
+
+ + Command Line +
+ {plugin.quickStart.http} +
+
- -
-
- - Docker Environment -
- {plugin.quickStart.docker} -
-
-
- ) : ( -
-
- - HTTP transport support coming soon -
-
- )} -
+ +
+
+ + Docker Environment +
+ {plugin.quickStart.docker} +
+
+ + ) : ( +
+
+ + HTTP transport support coming soon +
+
+ )} +
-
- - -
-
- - ); - })} -
-
+
+ + +
+ + + ) + })} +
+ - {/* Usage Patterns */} -
-
-

Usage Patterns

-

Multiple ways to integrate plugins into your workflow

-
+ {/* Usage Patterns */} +
+
+

Usage Patterns

+

Multiple ways to integrate plugins into your workflow

+
-
- - -
- -
- HTTP Transport - Maxim plugin only (for now) -
-
-
- -
- bifrost-http --plugins maxim -
-

Additional plugins coming soon

-
-
+
+ + +
+ +
+ HTTP Transport + Maxim plugin only (for now) +
+
+
+ +
+ bifrost-http --plugins maxim +
+

Additional plugins coming soon

+
+
- - -
- -
- Docker Deployment - Environment variables -
-
-
- -
- docker run -e APP_PLUGINS=maxim -
-

Additional plugins coming soon

-
-
+ + +
+ +
+ Docker Deployment + Environment variables +
+
+
+ +
+ docker run -e APP_PLUGINS=maxim +
+

Additional plugins coming soon

+
+
- - -
- -
- Go SDK - Full plugin ecosystem -
-
-
- -
- Plugins: []schemas.Plugin{`{...}`} -
-

All plugins available

-
-
-
-
+ + +
+ +
+ Go SDK + Full plugin ecosystem +
+
+
+ +
+ Plugins: []schemas.Plugin{`{...}`} +
+

All plugins available

+
+
+
+ - {/* Coming Soon */} -
-
-

Coming Soon

-

Exciting plugins currently in development

-
+ {/* Coming Soon */} +
+
+

Coming Soon

+

Exciting plugins currently in development

+
-
- {upcomingPlugins.map((plugin) => { - const Icon = plugin.icon; - return ( - - -
-
-
- -
-
- {plugin.name} - - Coming Soon - -
-
-
- {plugin.description} -
-
- ); - })} -
-
+
+ {upcomingPlugins.map((plugin) => { + const Icon = plugin.icon + return ( + + +
+
+
+ +
+
+ {plugin.name} + + Coming Soon + +
+
+
+ {plugin.description} +
+
+ ) + })} +
+
- {/* Community & Resources */} -
-
-

Join the Plugin Ecosystem

-

- Contribute to the growing collection of Bifrost plugins or build your own custom solutions -

+ {/* Community & Resources */} +
+
+

Join the Plugin Ecosystem

+

+ Contribute to the growing collection of Bifrost plugins or build your own custom solutions +

-
- - - -
-
-
-
-
-
- ); +
+ + + +
+
+ +
+ + + ) } diff --git a/ui/app/providers/page.tsx b/ui/app/providers/page.tsx new file mode 100644 index 0000000000..32464b396c --- /dev/null +++ b/ui/app/providers/page.tsx @@ -0,0 +1,34 @@ +'use client' + +import ProvidersList from '@/components/config/providers-list' +import FullPageLoader from '@/components/full-page-loader' +import { useToast } from '@/hooks/use-toast' +import { apiService } from '@/lib/api' +import { ProviderResponse } from '@/lib/types/config' +import { useEffect, useState } from 'react' + +export default function Providers() { + const [isLoadingProviders, setIsLoadingProviders] = useState(true) + const [providers, setProviders] = useState([]) + const { toast } = useToast() + + useEffect(() => { + loadProviders() + }, []) + + const loadProviders = async () => { + const [data, error] = await apiService.getProviders() + setIsLoadingProviders(false) + + if (error) { + toast({ + title: 'Error', + description: error, + variant: 'destructive', + }) + return + } + setProviders(data?.providers || []) + } + return
{isLoadingProviders ? : }
+} diff --git a/ui/components/config/mcp-client-form.tsx b/ui/components/config/mcp-client-form.tsx index 6d01d296ed..1217b33dfc 100644 --- a/ui/components/config/mcp-client-form.tsx +++ b/ui/components/config/mcp-client-form.tsx @@ -1,20 +1,19 @@ "use client"; -import React, { useEffect, useState } from "react"; -import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"; -import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; +import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; -import { Textarea } from "@/components/ui/textarea"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; -import { Alert, AlertDescription } from "@/components/ui/alert"; -import { MCPClient, CreateMCPClientRequest, UpdateMCPClientRequest, MCPConnectionType, MCPStdioConfig } from "@/lib/types/mcp"; -import { apiService } from "@/lib/api"; -import { useToast } from "@/hooks/use-toast"; +import { Textarea } from "@/components/ui/textarea"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; -import { Info, AlertTriangle } from "lucide-react"; +import { useToast } from "@/hooks/use-toast"; +import { apiService } from "@/lib/api"; +import { CreateMCPClientRequest, MCPClient, MCPConnectionType, MCPStdioConfig, UpdateMCPClientRequest } from "@/lib/types/mcp"; +import { isArrayEqual, parseArrayFromText } from "@/lib/utils/array"; import { Validator } from "@/lib/utils/validation"; -import { parseArrayFromText, isArrayEqual } from "@/lib/utils/array"; +import { Info } from "lucide-react"; +import React, { useEffect, useState } from "react"; interface ClientFormProps { client?: MCPClient | null; @@ -210,14 +209,7 @@ const ClientForm: React.FC = ({ client, open, onClose, onSaved {client ? "Edit MCP Client Tools" : "New MCP Client"} - - - - - Performance Notice: This operation may temporarily increase latency for incoming requests while being - processed. - - +
diff --git a/ui/components/config/mcp-clients-lists.tsx b/ui/components/config/mcp-clients-lists.tsx index 9d411b425d..f424075b1a 100644 --- a/ui/components/config/mcp-clients-lists.tsx +++ b/ui/components/config/mcp-clients-lists.tsx @@ -1,179 +1,183 @@ -"use client"; +'use client' -import React, { useEffect, useState } from "react"; -import { Button } from "@/components/ui/button"; -import { CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; -import { Badge } from "@/components/ui/badge"; -import { useToast } from "@/hooks/use-toast"; -import { apiService } from "@/lib/api"; -import { MCPClient } from "@/lib/types/mcp"; -import ClientForm from "@/components/config/mcp-client-form"; -import { Trash2, Pencil, Plus, RefreshCcw } from "lucide-react"; +import ClientForm from '@/components/config/mcp-client-form' +import { Badge } from '@/components/ui/badge' +import { Button } from '@/components/ui/button' +import { CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table' +import { useToast } from '@/hooks/use-toast' +import { apiService } from '@/lib/api' +import { MCP_STATUS_COLORS } from '@/lib/constants/config' +import { MCPClient } from '@/lib/types/mcp' +import { Pencil, Plus, RefreshCcw, Trash2 } from 'lucide-react' +import { useEffect, useState } from 'react' import { - AlertDialog, - AlertDialogDescription, - AlertDialogHeader, - AlertDialogCancel, - AlertDialogAction, - AlertDialogContent, - AlertDialogFooter, - AlertDialogTitle, - AlertDialogTrigger, -} from "../ui/alert-dialog"; -import { MCP_STATUS_COLORS } from "@/lib/constants/config"; + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from '../ui/alert-dialog' -export default function MCPClientsTab() { - const [clients, setClients] = useState([]); - const [selected, setSelected] = useState(null); - const [formOpen, setFormOpen] = useState(false); - const { toast } = useToast(); +interface MCPClientsListProps { + mcpClients: MCPClient[] +} + +export default function MCPClientsList({ mcpClients }: MCPClientsListProps) { + const [selected, setSelected] = useState(null) + const [clients, setClients] = useState(mcpClients) + const [formOpen, setFormOpen] = useState(false) + const { toast } = useToast() - const loadClients = async () => { - const [data, error] = await apiService.getMCPClients(); - if (error) { - toast({ title: "Error", description: error, variant: "destructive" }); - } else { - setClients(data || []); - } - }; + const loadClients = async () => { + const [data, error] = await apiService.getMCPClients() + if (error) { + toast({ title: 'Error', description: error, variant: 'destructive' }) + } else { + setClients(data || []) + } + } - useEffect(() => { - loadClients(); - }, []); + useEffect(() => { + loadClients() + }, []) - const handleCreate = () => { - setSelected(null); - setFormOpen(true); - }; + const handleCreate = () => { + setSelected(null) + setFormOpen(true) + } - const handleEdit = (client: MCPClient) => { - setSelected(client); - setFormOpen(true); - }; + const handleEdit = (client: MCPClient) => { + setSelected(client) + setFormOpen(true) + } - const handleReconnect = async (client: MCPClient) => { - const [, error] = await apiService.reconnectMCPClient(client.name); - if (error) { - toast({ title: "Error", description: error, variant: "destructive" }); - } else { - toast({ title: "Reconnected", description: "Client reconnected." }); - loadClients(); - } - }; + const handleReconnect = async (client: MCPClient) => { + const [, error] = await apiService.reconnectMCPClient(client.name) + if (error) { + toast({ title: 'Error', description: error, variant: 'destructive' }) + } else { + toast({ title: 'Reconnected', description: 'Client reconnected.' }) + loadClients() + } + } - const handleDelete = async (client: MCPClient) => { - const [, error] = await apiService.deleteMCPClient(client.name); - if (error) { - toast({ title: "Error", description: error, variant: "destructive" }); - } else { - toast({ title: "Deleted", description: "Client removed." }); - loadClients(); - } - }; + const handleDelete = async (client: MCPClient) => { + const [, error] = await apiService.deleteMCPClient(client.name) + if (error) { + toast({ title: 'Error', description: error, variant: 'destructive' }) + } else { + toast({ title: 'Deleted', description: 'Client removed.' }) + loadClients() + } + } - const handleSaved = () => { - setFormOpen(false); - loadClients(); - }; + const handleSaved = () => { + setFormOpen(false) + loadClients() + } - const getConnectionDisplay = (client: MCPClient) => { - if (client.config.connection_type === "stdio") { - return client.config.stdio_config?.command + " " + client.config.stdio_config?.args.join(" ") || "STDIO"; - } - return client.config.connection_string || `${client.config.connection_type.toUpperCase()}`; - }; + const getConnectionDisplay = (client: MCPClient) => { + if (client.config.connection_type === 'stdio') { + return `${client.config.stdio_config?.command} ${client.config.stdio_config?.args.join(' ')}` || 'STDIO' + } + return client.config.connection_string || `${client.config.connection_type.toUpperCase()}` + } - const getConnectionTypeDisplay = (type: string) => { - switch (type) { - case "http": - return "HTTP"; - case "sse": - return "SSE"; - case "stdio": - return "STDIO"; - default: - return type.toUpperCase(); - } - }; + const getConnectionTypeDisplay = (type: string) => { + switch (type) { + case 'http': + return 'HTTP' + case 'sse': + return 'SSE' + case 'stdio': + return 'STDIO' + default: + return type.toUpperCase() + } + } - return ( -
- - -
Registered Clients
- -
- Manage clients that can connect to the MCP Tools endpoint. -
-
- - - - Name - Connection Type - Connection Info - State - Actions - - - - {clients.length === 0 && ( - - - No clients found. - - - )} - {clients.map((c: MCPClient) => ( - - {c.name} - {getConnectionTypeDisplay(c.config.connection_type)} - {getConnectionDisplay(c)} - - {c.state} - - - {c.state === "disconnected" ? ( - - ) : ( - c.state === "connected" && ( - - ) - )} + return ( +
+ + +
Registered MCP Clients
+ +
+ Manage clients that can connect to the MCP Tools endpoint. +
+
+
+ + + Name + Connection Type + Connection Info + State + Actions + + + + {clients.length === 0 && ( + + + No clients found. + + + )} + {clients.map((c: MCPClient) => ( + + {c.name} + {getConnectionTypeDisplay(c.config.connection_type)} + {getConnectionDisplay(c)} + + {c.state} + + + {c.state === 'disconnected' ? ( + + ) : ( + c.state === 'connected' && ( + + ) + )} - - - - - - - Remove MCP Client - - Are you sure you want to remove MCP client {c.name}? You will need to reconnect the client to continue using it. - - - - Cancel - handleDelete(c)}>Delete - - - - - - ))} - -
-
- {formOpen && setFormOpen(false)} onSaved={handleSaved} />} -
- ); + + + + + + + Remove MCP Client + + Are you sure you want to remove MCP client {c.name}? You will need to reconnect the client to continue using it. + + + + Cancel + handleDelete(c)}>Delete + + + + + + ))} + + +
+ {formOpen && setFormOpen(false)} onSaved={handleSaved} />} +
+ ) } diff --git a/ui/components/config/provider-form.tsx b/ui/components/config/provider-form.tsx index 75a09ceef9..1b2c14da19 100644 --- a/ui/components/config/provider-form.tsx +++ b/ui/components/config/provider-form.tsx @@ -1,714 +1,745 @@ -"use client"; - -import { useEffect, useMemo, useState } from "react"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; -import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog"; -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; -import { TagInput } from "@/components/ui/tag-input"; -import { X, Plus, Save, Key, Globe, Zap, Info, AlertTriangle } from "lucide-react"; -import { toast } from "sonner"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +'use client' + +import { Button } from '@/components/ui/button' +import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog' +import { Input } from '@/components/ui/input' +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' +import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs' +import { TagInput } from '@/components/ui/tag-input' +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip' +import { apiService } from '@/lib/api' +import { DEFAULT_NETWORK_CONFIG, DEFAULT_PERFORMANCE_CONFIG } from '@/lib/constants/config' +import { ProviderIconType, renderProviderIcon } from '@/lib/constants/icons' +import { PROVIDER_LABELS, PROVIDERS as Providers } from '@/lib/constants/logs' import { - ProviderResponse, + AddProviderRequest, + ConcurrencyAndBufferSize, Key as KeyType, MetaConfig, - NetworkConfig, - ConcurrencyAndBufferSize, - AddProviderRequest, - UpdateProviderRequest, ModelProvider, + NetworkConfig, + ProviderResponse, ProxyConfig, ProxyType, -} from "@/lib/types/config"; -import { apiService } from "@/lib/api"; -import isEqual from "lodash.isequal"; -import { PROVIDER_LABELS } from "@/lib/constants/logs"; -import MetaConfigRenderer from "./meta-config-renderer"; -import { Validator } from "@/lib/utils/validation"; -import { renderProviderIcon, ProviderIconType } from "@/lib/constants/icons"; -import { PROVIDERS } from "@/lib/constants/logs"; -import { cn } from "@/lib/utils"; -import { Alert, AlertDescription } from "../ui/alert"; -import { DEFAULT_NETWORK_CONFIG, DEFAULT_PERFORMANCE_CONFIG } from "@/lib/constants/config"; + UpdateProviderRequest, +} from '@/lib/types/config' +import { cn } from '@/lib/utils' +import { Validator } from '@/lib/utils/validation' +import isEqual from 'lodash.isequal' +import { AlertTriangle, Globe, Info, Plus, Save, X, Zap } from 'lucide-react' +import { useEffect, useMemo, useState } from 'react' +import { toast } from 'sonner' +import { Alert, AlertDescription } from '../ui/alert' +import MetaConfigRenderer from './meta-config-renderer' interface ProviderFormProps { - provider?: ProviderResponse | null; - onSave: () => void; - onCancel: () => void; - existingProviders: string[]; + provider?: ProviderResponse | null + onSave: () => void + onCancel: () => void + existingProviders: string[] } // A helper function to create a clean initial state -const createInitialState = (provider?: ProviderResponse | null, defaultProvider?: string): Omit => { - const isNewProvider = !provider; - const providerName = provider?.name || defaultProvider || ""; - const keysRequired = !["vertex", "ollama"].includes(providerName); - - return { - selectedProvider: providerName, - keys: - isNewProvider && keysRequired - ? [{ value: "", models: [], weight: 1.0 }] - : !isNewProvider && keysRequired && provider?.keys - ? provider.keys - : [], - networkConfig: provider?.network_config || DEFAULT_NETWORK_CONFIG, - performanceConfig: provider?.concurrency_and_buffer_size || DEFAULT_PERFORMANCE_CONFIG, - metaConfig: provider?.meta_config || { - endpoint: "", - deployments: {}, - api_version: "", - }, - proxyConfig: provider?.proxy_config || { - type: "none", - url: "", - username: "", - password: "", - }, - }; -}; +const createInitialState = (provider?: ProviderResponse | null, defaultProvider?: string): Omit => { + const isNewProvider = !provider + const providerName = provider?.name || defaultProvider || '' + const keysRequired = !['vertex', 'ollama'].includes(providerName) + + return { + selectedProvider: providerName, + keys: + isNewProvider && keysRequired + ? [{ value: '', models: [], weight: 1.0 }] + : !isNewProvider && keysRequired && provider?.keys + ? provider.keys + : [], + networkConfig: provider?.network_config || DEFAULT_NETWORK_CONFIG, + performanceConfig: provider?.concurrency_and_buffer_size || DEFAULT_PERFORMANCE_CONFIG, + metaConfig: provider?.meta_config || { + endpoint: '', + deployments: {}, + api_version: '', + }, + proxyConfig: provider?.proxy_config || { + type: 'none', + url: '', + username: '', + password: '', + }, + } +} interface ProviderFormData { - selectedProvider: string; - keys: KeyType[]; - networkConfig: NetworkConfig; - performanceConfig: ConcurrencyAndBufferSize; - metaConfig: MetaConfig; - proxyConfig: ProxyConfig; - isDirty: boolean; + selectedProvider: string + keys: KeyType[] + networkConfig: NetworkConfig + performanceConfig: ConcurrencyAndBufferSize + metaConfig: MetaConfig + proxyConfig: ProxyConfig + isDirty: boolean } export default function ProviderForm({ provider, onSave, onCancel, existingProviders }: ProviderFormProps) { - // Find the first available provider if adding a new provider - const firstAvailableProvider = !provider ? PROVIDERS.find((p) => !existingProviders.includes(p)) || "" : undefined; - const [initialState] = useState>(createInitialState(provider, firstAvailableProvider)); - const [formData, setFormData] = useState({ - ...initialState, - isDirty: false, - }); - const [isLoading, setIsLoading] = useState(false); - - const { selectedProvider, keys, networkConfig, performanceConfig, metaConfig, proxyConfig, isDirty } = formData; - - const baseURLRequired = selectedProvider === "ollama"; - const keysRequired = !["vertex", "ollama"].includes(selectedProvider); - const keysValid = !keysRequired || keys.every((k) => k.value.trim() !== ""); - const keysPresent = !keysRequired || keys.length > 0; - - const performanceValid = - performanceConfig.concurrency > 0 && performanceConfig.buffer_size > 0 && performanceConfig.concurrency < performanceConfig.buffer_size; - - // Track if performance settings have changed - const performanceChanged = - performanceConfig.concurrency !== initialState.performanceConfig.concurrency || - performanceConfig.buffer_size !== initialState.performanceConfig.buffer_size; - - /* Meta configuration validation based on provider requirements */ - const getMetaValidation = () => { - let valid = true; - let message = ""; - - if (selectedProvider === "azure") { - const endpointValid = !!metaConfig.endpoint && (metaConfig.endpoint as string).trim() !== ""; - const deploymentsValid = !!( - metaConfig.deployments && - typeof metaConfig.deployments === "object" && - Object.keys(metaConfig.deployments as Record).length > 0 - ); - valid = endpointValid && deploymentsValid; - if (!valid) { - message = "Endpoint and at least one Deployment are required for Azure"; - } - } else if (selectedProvider === "bedrock") { - const regionValid = !!metaConfig.region && (metaConfig.region as string).trim() !== ""; - valid = regionValid; - if (!valid) { - message = "Region is required for AWS Bedrock"; - } - } else if (selectedProvider === "vertex") { - const projectValid = !!metaConfig.project_id && (metaConfig.project_id as string).trim() !== ""; - const credsValid = !!metaConfig.auth_credentials && (metaConfig.auth_credentials as string).trim() !== ""; - const regionValid = !!metaConfig.region && (metaConfig.region as string).trim() !== ""; - valid = projectValid && credsValid && regionValid; - if (!valid) { - message = "Project ID, Auth Credentials, and Region are required for Vertex AI"; - } - } - - return { valid, message }; - }; - - const { valid: metaValid, message: metaErrorMessage } = getMetaValidation(); - - useEffect(() => { - const currentData = { - selectedProvider, - keys: keysRequired ? keys : [], - networkConfig, - performanceConfig, - metaConfig, - proxyConfig, - }; - setFormData((prev) => ({ - ...prev, - isDirty: !isEqual(initialState, currentData), - })); - }, [selectedProvider, keys, networkConfig, performanceConfig, metaConfig, proxyConfig, initialState, keysRequired]); - - const updateField = (field: K, value: ProviderFormData[K]) => { - setFormData((prev) => ({ ...prev, [field]: value })); - }; - - const updateProxyField = (field: K, value: ProxyConfig[K]) => { - updateField("proxyConfig", { ...proxyConfig, [field]: value }); - }; - - const availableProviders = provider ? PROVIDERS : PROVIDERS.filter((p) => !existingProviders.includes(p)); - - const handleSubmit = async (e: React.FormEvent) => { - if (!validator.isValid()) { - toast.error(validator.getFirstError()); - return; - } - - e.preventDefault(); - setIsLoading(true); - - let error: string | null = null; - - if (provider) { - const data: UpdateProviderRequest = { - keys: keysRequired ? keys.filter((k) => k.value.trim() !== "") : [], - network_config: networkConfig, - concurrency_and_buffer_size: performanceConfig, - meta_config: metaConfig, - proxy_config: proxyConfig, - }; - [, error] = await apiService.updateProvider(provider.name, data); - } else { - const data: AddProviderRequest = { - provider: selectedProvider as ModelProvider, - keys: keysRequired ? keys.filter((k) => k.value.trim() !== "") : [], - network_config: networkConfig, - concurrency_and_buffer_size: performanceConfig, - meta_config: metaConfig, - proxy_config: proxyConfig, - }; - [, error] = await apiService.createProvider(data); - } - - setIsLoading(false); - - if (error) { - toast.error(error); - } else { - toast.success(`Provider ${provider ? "updated" : "added"} successfully`); - onSave(); - } - }; - - const validator = new Validator([ - // Provider selection - Validator.required(selectedProvider, "Please select a provider"), - - // Check if anything is dirty - Validator.custom(isDirty, "No changes to save"), - - // Base URL validation - ...(baseURLRequired - ? [ - Validator.required(networkConfig.base_url, "Base URL is required for Ollama provider"), - Validator.pattern(networkConfig.base_url || "", /^https?:\/\/.+/, "Base URL must start with http:// or https://"), - ] - : []), - - // API Keys validation - ...(keysRequired - ? [ - Validator.minValue(keys.length, 1, "At least one API key is required"), - Validator.custom( - keys.every((k) => k.value.trim() !== ""), - "API key value cannot be empty", - ), - ] - : []), - - // Network config validation - Validator.minValue(networkConfig.default_request_timeout_in_seconds, 1, "Timeout must be greater than 0 seconds"), - Validator.minValue(networkConfig.max_retries, 0, "Max retries cannot be negative"), - - // Performance config validation - Validator.minValue(performanceConfig.concurrency, 1, "Concurrency must be greater than 0"), - Validator.minValue(performanceConfig.buffer_size, 1, "Buffer size must be greater than 0"), - Validator.custom(performanceConfig.concurrency < performanceConfig.buffer_size, "Buffer size must be greater than concurrency"), - - // Meta config validation - Validator.custom(metaValid, metaErrorMessage), - - // Meta config validation for Azure - ...(selectedProvider === "azure" - ? [ - Validator.required(metaConfig.endpoint, "Azure endpoint is required"), - Validator.minValue( - Object.keys((metaConfig.deployments as Record) || {}).length, - 1, - "At least one Azure deployment is required", - ), - ] - : []), - - // Meta config validation for Bedrock - ...(selectedProvider === "bedrock" ? [Validator.required(metaConfig.region, "AWS region is required")] : []), - - // Meta config validation for Vertex - ...(selectedProvider === "vertex" - ? [ - Validator.required(metaConfig.project_id, "Project ID is required for Vertex AI"), - Validator.required(metaConfig.auth_credentials, "Auth credentials are required for Vertex AI"), - Validator.required(metaConfig.region, "Region is required for Vertex AI"), - ] - : []), - ]); - - const addKey = () => { - updateField("keys", [...keys, { value: "", models: [], weight: 1.0 }]); - }; - - const removeKey = (index: number) => { - updateField( - "keys", - keys.filter((_, i) => i !== index), - ); - }; - - const updateKey = (index: number, field: keyof KeyType, value: string | number | string[]) => { - const newKeys = [...keys]; - const keyToUpdate = { ...newKeys[index] }; - - if (field === "models" && Array.isArray(value)) { - keyToUpdate.models = value; - } else if (field === "value" && typeof value === "string") { - keyToUpdate.value = value; - } else if (field === "weight" && typeof value === "string") { - keyToUpdate.weight = parseFloat(value) || 1.0; - } - - newKeys[index] = keyToUpdate; - updateField("keys", newKeys); - }; - - const handleMetaConfigChange = (field: keyof MetaConfig, value: string | Record) => { - updateField("metaConfig", { ...metaConfig, [field]: value }); - }; - - const tabs = useMemo(() => { - const availableTabs = []; - - // Only add API Keys tab if required for this provider - if (keysRequired) { - availableTabs.push({ - id: "api-keys", - label: "API Keys", - }); - } - - // Add Meta Config tab for providers that need it - if (selectedProvider === "azure" || selectedProvider === "bedrock" || selectedProvider === "vertex") { - availableTabs.push({ - id: "meta-config", - label: "Meta Config", - }); - } - - // Network tab is always available - availableTabs.push({ - id: "network", - label: "Network", - }); - - // Performance tab is always available - availableTabs.push({ - id: "performance", - label: "Performance", - }); - - return availableTabs; - }, [keysRequired, selectedProvider]); - - const [selectedTab, setSelectedTab] = useState(tabs[0]?.id || "api-keys"); - - useEffect(() => { - if (!tabs.map((t) => t.id).includes(selectedTab)) { - setSelectedTab(tabs[0]?.id || "api-keys"); - } - }, [tabs]); - - return ( - - - - - {provider ? ( -
- {renderProviderIcon(provider.name as ProviderIconType, { size: 20 })} - {PROVIDER_LABELS[provider.name]} -
- ) : ( -
Add Provider
- )} -
- Configure AI provider settings, API keys, and network options. -
- -
- {/* Provider Selection */} - {!provider && - (availableProviders.length === 0 ? ( -
All providers have been configured.
- ) : ( - -
- {PROVIDERS.map((p) => ( - - { - e.preventDefault(); - if (availableProviders.includes(p)) { - updateField("selectedProvider", p); - } - }} - asChild - > - - {renderProviderIcon(p as ProviderIconType, { size: "sm" })} -
{PROVIDER_LABELS[p as keyof typeof PROVIDER_LABELS]}
-
-
- {!availableProviders.includes(p) && Provider is already configured} -
- ))} -
-
- ))} - - - - {tabs.map((tab) => ( - - {tab.label} - - ))} - - - {/* API Keys Tab */} - {keysRequired && ( - -
-
- -

API Keys

- - - - - - - - -

- Use env.<VAR> to read the - value from an environment variable. -

-
-
-
-
- -
-
- {keys.map((key, index) => ( -
-
-
-
API Key
- updateKey(index, "value", e.target.value)} - type="text" - className={`flex-1 ${keysRequired && key.value.trim() === "" ? "border-destructive" : ""}`} - /> -
-
-
- - - - - - - - - -

Determines traffic distribution between keys. Higher weights receive more requests.

-
-
-
-
- updateKey(index, "weight", e.target.value)} - type="number" - step="0.1" - min="0" - max="1.0" - className="w-20" - /> -
-
-
-
- - - - - - - - - -

Comma-separated list of models this key applies to. Leave blank for all models.

-
-
-
-
- updateKey(index, "models", newModels)} - /> -
- {keys.length > 1 && ( - - )} -
- ))} -
-
- )} - - {/* Meta Config Tab */} - {selectedProvider !== "anthropic" && selectedProvider !== "openai" && selectedProvider !== "cohere" && ( - - - - )} - - {/* Network Tab */} - - {/* Network Configuration */} -
-
- -

Network Configuration

-
-
-
- - - updateField("networkConfig", { - ...networkConfig, - base_url: e.target.value, - }) - } - className={baseURLRequired && !networkConfig.base_url ? "border-destructive" : ""} - /> -
-
-
- - - updateField("networkConfig", { - ...networkConfig, - default_request_timeout_in_seconds: parseInt(e.target.value) || 30, - }) - } - /> -
-
- - - updateField("networkConfig", { - ...networkConfig, - max_retries: parseInt(e.target.value) || 0, - }) - } - /> -
-
-
-
- - {/* Proxy Configuration */} -
-
- -

Proxy Settings

-
-
-
- - -
- - {proxyConfig.type !== "none" && proxyConfig.type !== "environment" && ( -
-
- - updateProxyField("url", e.target.value)} - /> -
-
-
- - updateProxyField("username", e.target.value)} - placeholder="Proxy username" - /> -
-
- - updateProxyField("password", e.target.value)} - placeholder="Proxy password" - /> -
-
-
- )} -
-
-
- - {/* Performance Tab */} - -
- -

Performance Settings

-
- {performanceChanged && ( - - - - Heads up: Changing concurrency or buffer size may temporarily affect request latency for this provider - while the new settings are being applied. - - - )} -
-
- - - updateField("performanceConfig", { - ...performanceConfig, - concurrency: parseInt(e.target.value) || 0, - }) - } - className={!performanceValid ? "border-destructive" : ""} - /> -
-
- - - updateField("performanceConfig", { - ...performanceConfig, - buffer_size: parseInt(e.target.value) || 0, - }) - } - className={!performanceValid ? "border-destructive" : ""} - /> -
-
-
-
- - {/* Form Actions */} - {availableProviders.length > 0 && ( -
- - - - - - - - - {(!validator.isValid() || isLoading) && ( - -

{isLoading ? "Saving..." : validator.getFirstError() || "Please fix validation errors"}

-
- )} -
-
-
- )} -
-
-
- ); + // Find the first available provider if adding a new provider + const firstAvailableProvider = !provider ? Providers.find((p) => !existingProviders.includes(p)) || '' : undefined + const [initialState] = useState>(createInitialState(provider, firstAvailableProvider)) + const [formData, setFormData] = useState({ + ...initialState, + isDirty: false, + }) + const [isLoading, setIsLoading] = useState(false) + + const { selectedProvider, keys, networkConfig, performanceConfig, metaConfig, proxyConfig, isDirty } = formData + + const baseURLRequired = selectedProvider === 'ollama' + const keysRequired = !['vertex', 'ollama'].includes(selectedProvider) + const keysValid = !keysRequired || keys.every((k) => k.value.trim() !== '') + const keysPresent = !keysRequired || keys.length > 0 + + const performanceValid = + performanceConfig.concurrency > 0 && performanceConfig.buffer_size > 0 && performanceConfig.concurrency < performanceConfig.buffer_size + + // Track if performance settings have changed + const performanceChanged = + performanceConfig.concurrency !== initialState.performanceConfig.concurrency || + performanceConfig.buffer_size !== initialState.performanceConfig.buffer_size + + /* Meta configuration validation based on provider requirements */ + const getMetaValidation = () => { + let valid = true + let message = '' + + if (selectedProvider === 'azure') { + const endpointValid = !!metaConfig.endpoint && (metaConfig.endpoint as string).trim() !== '' + const deploymentsValid = !!( + metaConfig.deployments && + typeof metaConfig.deployments === 'object' && + Object.keys(metaConfig.deployments as Record).length > 0 + ) + valid = endpointValid && deploymentsValid + if (!valid) { + message = 'Endpoint and at least one Deployment are required for Azure' + } + } else if (selectedProvider === 'bedrock') { + const regionValid = !!metaConfig.region && (metaConfig.region as string).trim() !== '' + valid = regionValid + if (!valid) { + message = 'Region is required for AWS Bedrock' + } + } else if (selectedProvider === 'vertex') { + const projectValid = !!metaConfig.project_id && (metaConfig.project_id as string).trim() !== '' + const credsValid = !!metaConfig.auth_credentials && (metaConfig.auth_credentials as string).trim() !== '' + const regionValid = !!metaConfig.region && (metaConfig.region as string).trim() !== '' + valid = projectValid && credsValid && regionValid + if (!valid) { + message = 'Project ID, Auth Credentials, and Region are required for Vertex AI' + } + } + + return { valid, message } + } + + const { valid: metaValid, message: metaErrorMessage } = getMetaValidation() + + useEffect(() => { + const currentData = { + selectedProvider, + keys: keysRequired ? keys : [], + networkConfig, + performanceConfig, + metaConfig, + proxyConfig, + } + setFormData((prev) => ({ + ...prev, + isDirty: !isEqual(initialState, currentData), + })) + }, [selectedProvider, keys, networkConfig, performanceConfig, metaConfig, proxyConfig, initialState, keysRequired]) + + const updateField = (field: K, value: ProviderFormData[K]) => { + setFormData((prev) => ({ ...prev, [field]: value })) + } + + const updateProxyField = (field: K, value: ProxyConfig[K]) => { + updateField('proxyConfig', { ...proxyConfig, [field]: value }) + } + + const availableProviders = provider ? Providers : Providers.filter((p) => !existingProviders.includes(p)) + + const handleSubmit = async (e: React.FormEvent) => { + if (!validator.isValid()) { + toast.error(validator.getFirstError()) + return + } + + e.preventDefault() + setIsLoading(true) + + let error: string | null = null + + if (provider) { + const data: UpdateProviderRequest = { + keys: keysRequired ? keys.filter((k) => k.value.trim() !== '') : [], + network_config: networkConfig, + concurrency_and_buffer_size: performanceConfig, + meta_config: metaConfig, + proxy_config: proxyConfig, + } + ;[, error] = await apiService.updateProvider(provider.name, data) + } else { + const data: AddProviderRequest = { + provider: selectedProvider as ModelProvider, + keys: keysRequired ? keys.filter((k) => k.value.trim() !== '') : [], + network_config: networkConfig, + concurrency_and_buffer_size: performanceConfig, + meta_config: metaConfig, + proxy_config: proxyConfig, + } + ;[, error] = await apiService.createProvider(data) + } + + setIsLoading(false) + + if (error) { + toast.error(error) + } else { + toast.success(`Provider ${provider ? 'updated' : 'added'} successfully`) + onSave() + } + } + + const validator = new Validator([ + // Provider selection + Validator.required(selectedProvider, 'Please select a provider'), + + // Check if anything is dirty + Validator.custom(isDirty, 'No changes to save'), + + // Base URL validation + ...(baseURLRequired + ? [ + Validator.required(networkConfig.base_url, 'Base URL is required for Ollama provider'), + Validator.pattern(networkConfig.base_url || '', /^https?:\/\/.+/, 'Base URL must start with http:// or https://'), + ] + : []), + + // API Keys validation + ...(keysRequired + ? [ + Validator.minValue(keys.length, 1, 'At least one API key is required'), + Validator.custom( + keys.every((k) => k.value.trim() !== ''), + 'API key value cannot be empty', + ), + ] + : []), + + // Network config validation + Validator.minValue(networkConfig.default_request_timeout_in_seconds, 1, 'Timeout must be greater than 0 seconds'), + Validator.minValue(networkConfig.max_retries, 0, 'Max retries cannot be negative'), + + // Performance config validation + Validator.minValue(performanceConfig.concurrency, 1, 'Concurrency must be greater than 0'), + Validator.minValue(performanceConfig.buffer_size, 1, 'Buffer size must be greater than 0'), + Validator.custom(performanceConfig.concurrency < performanceConfig.buffer_size, 'Buffer size must be greater than concurrency'), + + // Meta config validation + Validator.custom(metaValid, metaErrorMessage), + + // Meta config validation for Azure + ...(selectedProvider === 'azure' + ? [ + Validator.required(metaConfig.endpoint, 'Azure endpoint is required'), + Validator.minValue( + Object.keys((metaConfig.deployments as Record) || {}).length, + 1, + 'At least one Azure deployment is required', + ), + ] + : []), + + // Meta config validation for Bedrock + ...(selectedProvider === 'bedrock' ? [Validator.required(metaConfig.region, 'AWS region is required')] : []), + + // Meta config validation for Vertex + ...(selectedProvider === 'vertex' + ? [ + Validator.required(metaConfig.project_id, 'Project ID is required for Vertex AI'), + Validator.required(metaConfig.auth_credentials, 'Auth credentials are required for Vertex AI'), + Validator.required(metaConfig.region, 'Region is required for Vertex AI'), + ] + : []), + ]) + + const addKey = () => { + updateField('keys', [...keys, { value: '', models: [], weight: 1.0 }]) + } + + const removeKey = (index: number) => { + updateField( + 'keys', + keys.filter((_, i) => i !== index), + ) + } + + const updateKey = (index: number, field: keyof KeyType, value: string | number | string[]) => { + const newKeys = [...keys] + const keyToUpdate = { ...newKeys[index] } + + if (field === 'models' && Array.isArray(value)) { + keyToUpdate.models = value + } else if (field === 'value' && typeof value === 'string') { + keyToUpdate.value = value + } else if (field === 'weight' && typeof value === 'string') { + keyToUpdate.weight = Number.parseFloat(value) || 1.0 + } + + newKeys[index] = keyToUpdate + updateField('keys', newKeys) + } + + const handleMetaConfigChange = (field: keyof MetaConfig, value: string | Record) => { + updateField('metaConfig', { ...metaConfig, [field]: value }) + } + + const tabs = useMemo(() => { + const availableTabs = [] + + // Only add API Keys tab if required for this provider + if (keysRequired) { + availableTabs.push({ + id: 'api-keys', + label: 'API Keys', + }) + } + + // Add Meta Config tab for providers that need it + if (selectedProvider === 'azure' || selectedProvider === 'bedrock' || selectedProvider === 'vertex') { + availableTabs.push({ + id: 'meta-config', + label: 'Meta Config', + }) + } + + // Network tab is always available + availableTabs.push({ + id: 'network', + label: 'Network', + }) + + // Performance tab is always available + availableTabs.push({ + id: 'performance', + label: 'Performance', + }) + + return availableTabs + }, [keysRequired, selectedProvider]) + + const [selectedTab, setSelectedTab] = useState(tabs[0]?.id || 'api-keys') + + useEffect(() => { + if (!tabs.map((t) => t.id).includes(selectedTab)) { + setSelectedTab(tabs[0]?.id || 'api-keys') + } + }, [tabs]) + + return ( + + + + + {provider ? ( +
+ {renderProviderIcon(provider.name as ProviderIconType, { size: 20 })} + {PROVIDER_LABELS[provider.name]} +
+ ) : ( +
Add Provider
+ )} +
+ Configure AI provider settings, API keys, and network options. +
+ +
+ {/* Provider Selection */} + {!provider && ( + +
+ {Providers.map((p) => ( + + { + e.preventDefault() + if (availableProviders.includes(p)) { + updateField('selectedProvider', p) + } + }} + asChild + > + + {renderProviderIcon(p as ProviderIconType, { size: 'sm', className: 'w-5 h-5' })} +
{PROVIDER_LABELS[p as keyof typeof PROVIDER_LABELS]}
+
+
+ {!availableProviders.includes(p) && Provider is already configured} +
+ ))} +
+
+ )} + +
+ + + {tabs.map((tab) => ( + + {tab.label} + + ))} + + + {/* Animated Container for Tab Content */} +
+
+ {/* API Keys Tab */} + {keysRequired && selectedTab === 'api-keys' && ( +
+
+
+

API Keys

+ + + + + + + + +

+ Use env.<VAR> to read the + value from an environment variable. +

+
+
+
+
+ +
+
+ {keys.map((key, index) => ( +
+
+
+
API Key
+ updateKey(index, 'value', e.target.value)} + type="text" + className={`flex-1 transition-all duration-200 ease-in-out ${keysRequired && key.value.trim() === '' ? 'border-destructive' : ''}`} + /> +
+
+
+ + + + + + + + + +

Determines traffic distribution between keys. Higher weights receive more requests.

+
+
+
+
+ updateKey(index, 'weight', e.target.value)} + type="number" + step="0.1" + min="0" + max="1.0" + className="w-20 transition-all duration-200 ease-in-out" + /> +
+
+
+
+ + + + + + + + + +

Comma-separated list of models this key applies to. Leave blank for all models.

+
+
+
+
+ updateKey(index, 'models', newModels)} + /> +
+ {keys.length > 1 && ( + + )} +
+ ))} +
+
+ )} + + {/* Meta Config Tab */} + {selectedProvider !== 'anthropic' && selectedProvider !== 'openai' && selectedProvider !== 'cohere' && selectedTab === 'meta-config' && ( +
+ +
+ )} + + {/* Network Tab */} + {selectedTab === 'network' && ( +
+ {/* Network Configuration */} +
+
+ +

Network Configuration

+
+
+
+ + + updateField('networkConfig', { + ...networkConfig, + base_url: e.target.value, + }) + } + className={`transition-all duration-200 ease-in-out ${baseURLRequired && !networkConfig.base_url ? 'border-destructive' : ''}`} + /> +
+
+
+ + + updateField('networkConfig', { + ...networkConfig, + default_request_timeout_in_seconds: Number.parseInt(e.target.value) || 30, + }) + } + className="transition-all duration-200 ease-in-out" + /> +
+
+ + + updateField('networkConfig', { + ...networkConfig, + max_retries: Number.parseInt(e.target.value) || 0, + }) + } + className="transition-all duration-200 ease-in-out" + /> +
+
+
+
+ + {/* Proxy Configuration */} +
+
+ +

Proxy Settings

+
+
+
+ + +
+ +
+
+
+ + updateProxyField('url', e.target.value)} + className="transition-all duration-200 ease-in-out" + /> +
+
+
+ + updateProxyField('username', e.target.value)} + placeholder="Proxy username" + className="transition-all duration-200 ease-in-out" + /> +
+
+ + updateProxyField('password', e.target.value)} + placeholder="Proxy password" + className="transition-all duration-200 ease-in-out" + /> +
+
+
+
+
+
+
+ )} + + {/* Performance Tab */} + {selectedTab === 'performance' && ( +
+
+ +

Performance Settings

+
+
+ + + + Heads up: Changing concurrency or buffer size may temporarily affect request latency for this + provider while the new settings are being applied. + + +
+
+
+ + + updateField('performanceConfig', { + ...performanceConfig, + concurrency: Number.parseInt(e.target.value) || 0, + }) + } + className={`transition-all duration-200 ease-in-out ${!performanceValid ? 'border-destructive' : ''}`} + /> +
+
+ + + updateField('performanceConfig', { + ...performanceConfig, + buffer_size: Number.parseInt(e.target.value) || 0, + }) + } + className={`transition-all duration-200 ease-in-out ${!performanceValid ? 'border-destructive' : ''}`} + /> +
+
+
+ )} +
+
+
+ + {/* Form Actions */} +
+ {availableProviders.length > 0 && ( +
+ + + + + + + + + {(!validator.isValid() || isLoading) && ( + +

{isLoading ? 'Saving...' : validator.getFirstError() || 'Please fix validation errors'}

+
+ )} +
+
+
+ )} +
+
+
+
+
+ ) } diff --git a/ui/components/config/providers-list.tsx b/ui/components/config/providers-list.tsx index 2b621ced94..521f63d0cc 100644 --- a/ui/components/config/providers-list.tsx +++ b/ui/components/config/providers-list.tsx @@ -1,28 +1,28 @@ "use client"; -import { useState } from "react"; -import { Button } from "@/components/ui/button"; -import { Badge } from "@/components/ui/badge"; -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; -import { Edit, Trash2, Key, Loader2, Plus } from "lucide-react"; -import { toast } from "sonner"; -import { ProviderResponse } from "@/lib/types/config"; -import { apiService } from "@/lib/api"; -import { PROVIDER_LABELS } from "@/lib/constants/logs"; import { AlertDialog, - AlertDialogTrigger, + AlertDialogAction, + AlertDialogCancel, AlertDialogContent, - AlertDialogHeader, - AlertDialogTitle, AlertDialogDescription, AlertDialogFooter, - AlertDialogCancel, - AlertDialogAction, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, } from "@/components/ui/alert-dialog"; -import { CardHeader, CardTitle, CardDescription } from "../ui/card"; -import ProviderForm from "./provider-form"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { apiService } from "@/lib/api"; import { ProviderIconType, renderProviderIcon } from "@/lib/constants/icons"; +import { PROVIDER_LABELS } from "@/lib/constants/logs"; +import { ProviderResponse } from "@/lib/types/config"; +import { Edit, Key, Loader2, Plus, Trash2 } from "lucide-react"; +import { useState } from "react"; +import { toast } from "sonner"; +import { CardDescription, CardHeader, CardTitle } from "../ui/card"; +import ProviderForm from "./provider-form"; interface ProvidersListProps { providers: ProviderResponse[]; @@ -68,7 +68,7 @@ export default function ProvidersList({ providers, onRefresh }: ProvidersListPro {showProviderForm && ( setShowProviderForm(false)} existingProviders={providers.map((p) => p.name)} /> diff --git a/ui/components/logs/empty-state.tsx b/ui/components/logs/empty-state.tsx index 847e7a09d7..0235c96f64 100644 --- a/ui/components/logs/empty-state.tsx +++ b/ui/components/logs/empty-state.tsx @@ -1,117 +1,93 @@ -"use client"; - -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { Card } from "@/components/ui/card"; -import { Button } from "@/components/ui/button"; -import { Copy, RefreshCw, ArrowRight, AlertTriangle } from "lucide-react"; -import { CodeEditor } from "./ui/code-editor"; -import { toast } from "sonner"; -import { useState, useMemo } from "react"; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; -import { Alert, AlertDescription } from "../ui/alert"; -import { getExampleBaseUrl } from "@/lib/utils/port"; - -type Provider = "openai" | "anthropic" | "genai" | "litellm"; -type Language = "python" | "typescript"; +'use client' + +import { Button } from '@/components/ui/button' +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' +import { getExampleBaseUrl } from '@/lib/utils/port' +import { AlertTriangle, Copy } from 'lucide-react' +import { useMemo, useState } from 'react' +import { toast } from 'sonner' +import { Alert, AlertDescription } from '../ui/alert' +import { CodeEditor } from './ui/code-editor' + +type Provider = 'openai' | 'anthropic' | 'genai' | 'litellm' +type Language = 'python' | 'typescript' type Examples = { - curl: string; - sdk: { - [P in Provider]: { - [L in Language]: string; - }; - }; -}; + curl: string + sdk: { + [P in Provider]: { + [L in Language]: string + } + } +} // Common editor options to reduce duplication const EDITOR_OPTIONS = { - scrollBeyondLastLine: false, - minimap: { enabled: false }, - lineNumbers: "off", - folding: false, - lineDecorationsWidth: 0, - lineNumbersMinChars: 0, - glyphMargin: false, -} as const; + scrollBeyondLastLine: false, + minimap: { enabled: false }, + lineNumbers: 'off', + folding: false, + lineDecorationsWidth: 0, + lineNumbersMinChars: 0, + glyphMargin: false, +} as const interface CodeBlockProps { - code: string; - language: string; - onLanguageChange?: (language: string) => void; - showLanguageSelect?: boolean; - readonly?: boolean; + code: string + language: string + onLanguageChange?: (language: string) => void + showLanguageSelect?: boolean + readonly?: boolean } function CodeBlock({ code, language, onLanguageChange, showLanguageSelect = false, readonly = true }: CodeBlockProps) { - const copyToClipboard = () => { - navigator.clipboard.writeText(code); - toast.success("Copied to clipboard"); - }; - - return ( -
-
- {showLanguageSelect && onLanguageChange && ( - - )} - -
- -
- ); + const copyToClipboard = () => { + navigator.clipboard.writeText(code) + toast.success('Copied to clipboard') + } + + return ( +
+
+ {showLanguageSelect && onLanguageChange && ( + + )} + +
+ +
+ ) } -const CARDS = [ - { - title: "What You'll See Here", - description: "Real-time request logs from all your API calls", - features: [ - "Real-time request logs from all your API calls", - "Comprehensive request and error details", - "Token usage, latency, and cost metrics", - "Advanced filtering and search capabilities", - ], - }, - { - title: "Getting Started", - description: "Use the examples below to get started", - features: [ - "Choose an example from below", - "Set Bifrost as your API endpoint", - "Send a test request", - "Monitor the response in real-time", - ], - }, -]; - interface EmptyStateProps { - isSocketConnected: boolean; - error: string | null; + isSocketConnected: boolean + error: string | null } export function EmptyState({ isSocketConnected, error }: EmptyStateProps) { - const [language, setLanguage] = useState("python"); + const [language, setLanguage] = useState('python') - // Generate examples dynamically using the port utility - const examples: Examples = useMemo(() => { - const baseUrl = getExampleBaseUrl(); + // Generate examples dynamically using the port utility + const examples: Examples = useMemo(() => { + const baseUrl = getExampleBaseUrl() - return { - curl: `curl -X POST ${baseUrl}/v1/chat/completions \\ + return { + curl: `curl -X POST ${baseUrl}/v1/chat/completions \\ -H "Content-Type: application/json" \\ -d '{ "model": "openai/gpt-4o-mini", @@ -119,9 +95,9 @@ export function EmptyState({ isSocketConnected, error }: EmptyStateProps) { {"role": "user", "content": "Hello!"} ] }'`, - sdk: { - openai: { - python: `import openai + sdk: { + openai: { + python: `import openai client = openai.OpenAI( base_url="${baseUrl}/openai", @@ -132,7 +108,7 @@ response = client.chat.completions.create( model="gpt-4o-mini", # or "provider/model" for other providers (anthropic/claude-3-sonnet) messages=[{"role": "user", "content": "Hello!"}] )`, - typescript: `import OpenAI from "openai"; + typescript: `import OpenAI from "openai"; const openai = new OpenAI({ baseURL: "${baseUrl}/openai", @@ -143,9 +119,9 @@ const response = await openai.chat.completions.create({ model: "gpt-4o-mini", // or "provider/model" for other providers (anthropic/claude-3-sonnet) messages: [{ role: "user", content: "Hello!" }], });`, - }, - anthropic: { - python: `import anthropic + }, + anthropic: { + python: `import anthropic client = anthropic.Anthropic( base_url="${baseUrl}/anthropic", @@ -157,7 +133,7 @@ response = client.messages.create( max_tokens=1000, messages=[{"role": "user", "content": "Hello!"}] )`, - typescript: `import Anthropic from "@anthropic-ai/sdk"; + typescript: `import Anthropic from "@anthropic-ai/sdk"; const anthropic = new Anthropic({ baseURL: "${baseUrl}/anthropic", @@ -169,9 +145,9 @@ const response = await anthropic.messages.create({ max_tokens: 1000, messages: [{ role: "user", content: "Hello!" }], });`, - }, - genai: { - python: `from google import genai + }, + genai: { + python: `from google import genai from google.genai.types import HttpOptions client = genai.Client( @@ -183,7 +159,7 @@ response = client.models.generate_content( model="gemini-2.5-pro", # or "provider/model" for other providers (openai/gpt-4o-mini) contents="Hello!" )`, - typescript: `import { GoogleGenerativeAI } from "@google/generative-ai"; + typescript: `import { GoogleGenerativeAI } from "@google/generative-ai"; const genAI = new GoogleGenerativeAI("dummy-api-key", { // Handled by Bifrost baseUrl: "${baseUrl}/genai", @@ -191,9 +167,9 @@ const genAI = new GoogleGenerativeAI("dummy-api-key", { // Handled by Bifrost const model = genAI.getGenerativeModel({ model: "gemini-2.5-pro" }); // or "provider/model" for other providers (openai/gpt-4o-mini) const response = await model.generateContent("Hello!");`, - }, - litellm: { - python: `import litellm + }, + litellm: { + python: `import litellm litellm.api_base = "${baseUrl}/litellm" @@ -201,112 +177,96 @@ response = litellm.completion( model="openai/gpt-4o-mini", messages=[{"role": "user", "content": "Hello!"}] )`, - typescript: `import { completion } from "litellm"; + typescript: `import { completion } from "litellm"; const response = await completion({ model: "openai/gpt-4o-mini", messages: [{ role: "user", content: "Hello!" }], api_base: "${baseUrl}/litellm", });`, - }, - }, - }; - }, []); - - return ( -
-
-

Welcome to Request Logs

-

Monitor and analyze all your API requests in real-time

-
- - {isSocketConnected && ( -
- - Listening for logs... -
- )} - - {error && ( - - - {error} - - )} - -
- {CARDS.map((card) => ( - -

{card.title}

-

{card.description}

-
    - {card.features.map((feature) => ( -
  • - - {feature} -
  • - ))} -
-
- ))} -
- -
-
-

Integration Examples

-

Send your first request to get started

-
- - - - cURL - OpenAI SDK - Anthropic SDK - Google GenAI SDK - LiteLLM SDK - - - - - - - - setLanguage(newLang as Language)} - showLanguageSelect - /> - - - - setLanguage(newLang as Language)} - showLanguageSelect - /> - - - - setLanguage(newLang as Language)} - showLanguageSelect - /> - - - - setLanguage(newLang as Language)} - showLanguageSelect - /> - - -
-
- ); + }, + }, + } + }, []) + + return ( +
+ {error && ( + + + {error} + + )} + +
+
+
+

Integration under 60 seconds

+

Send your first request to get started

+
+
+ {isSocketConnected && ( +
+ + + + + Listening for logs... +
+ )} +
+
+ + + + cURL + OpenAI SDK + Anthropic SDK + Google GenAI SDK + LiteLLM SDK + + + + + + + + setLanguage(newLang as Language)} + showLanguageSelect + /> + + + + setLanguage(newLang as Language)} + showLanguageSelect + /> + + + + setLanguage(newLang as Language)} + showLanguageSelect + /> + + + + setLanguage(newLang as Language)} + showLanguageSelect + /> + + +
+
+ ) } diff --git a/ui/components/sidebar.tsx b/ui/components/sidebar.tsx index 7ca5051055..585d824c9c 100644 --- a/ui/components/sidebar.tsx +++ b/ui/components/sidebar.tsx @@ -1,49 +1,79 @@ "use client"; -import { Home, BookOpen, Settings, Puzzle, ExternalLink, HeartHandshake } from "lucide-react"; +import { BoxIcon, BugIcon, ExternalLink, Puzzle, Settings2Icon, Telescope } from "lucide-react"; +import { Badge } from "@/components/ui/badge"; import { Sidebar, SidebarContent, + SidebarFooter, SidebarGroup, SidebarGroupContent, SidebarGroupLabel, SidebarHeader, SidebarMenu, SidebarMenuButton, - SidebarMenuItem, - SidebarSeparator, - SidebarFooter, + SidebarMenuItem } from "@/components/ui/sidebar"; -import { Badge } from "@/components/ui/badge"; -import { usePathname } from "next/navigation"; -import Link from "next/link"; +import { useWebSocket } from "@/hooks/useWebSocket"; import { cn } from "@/lib/utils"; +import { BooksIcon, DiscordLogoIcon, GithubLogoIcon } from "@phosphor-icons/react"; import { useTheme } from "next-themes"; -import { useState, useEffect } from "react"; import Image from "next/image"; +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { useEffect, useState } from "react"; import { ThemeToggle } from "./theme-toggle"; -import { useWebSocket } from "@/hooks/useWebSocket"; -import { BookOpenTextIcon, DiscordLogoIcon, GithubLogoIcon } from "@phosphor-icons/react"; + +// Custom MCP Icon Component +const MCPIcon = ({ className }: { className?: string }) => ( + + MCP clients icon + + + +); // Main navigation items const navigationItems = [ { title: "Logs", url: "/", - icon: Home, + icon: Telescope, description: "Request logs & monitoring", }, + { + title: "Providers", + url: "/providers", + icon: BoxIcon, + description: "Configure models", + }, + { + title: "MCP clients", + url: "/mcp-clients", + icon: MCPIcon, + description: "MCP configuration", + }, { title: "Config", url: "/config", - icon: Settings, - description: "Providers & MCP configuration", + icon: Settings2Icon, + description: "Bifrost settings", }, { title: "Docs", url: "/docs", - icon: BookOpen, + icon: BooksIcon, description: "Documentation & guides", }, { @@ -67,10 +97,15 @@ const externalLinks = [ url: "https://github.com/maximhq/bifrost", icon: GithubLogoIcon, }, + { + title: "Report a bug", + url: "https://github.com/maximhq/bifrost/issues/new?title=[Bug Report]&labels=bug&type=bug&projects=maximhq/1", + icon: BugIcon, + }, { title: "Full Documentation", url: "https://github.com/maximhq/bifrost/tree/main/docs", - icon: BookOpenTextIcon, + icon: BooksIcon, }, ]; @@ -107,9 +142,6 @@ export default function AppSidebar() { - - Navigation - {navigationItems.map((item) => { @@ -120,7 +152,7 @@ export default function AppSidebar() { asChild className={`relative h-16 rounded-lg border px-3 transition-all duration-200 ${ isActive - ? "bg-accent text-primary border-primary/20 shadow-sm" + ? "bg-accent text-primary border-primary/10" : "hover:bg-accent hover:text-accent-foreground border-transparent" } `} > @@ -139,7 +171,7 @@ export default function AppSidebar() { )} {item.badge && ( {item.badge} @@ -154,8 +186,6 @@ export default function AppSidebar() { - - Resources diff --git a/ui/components/ui/dialog.tsx b/ui/components/ui/dialog.tsx index f47272c4a6..de8506c01c 100644 --- a/ui/components/ui/dialog.tsx +++ b/ui/components/ui/dialog.tsx @@ -1,8 +1,8 @@ "use client"; -import * as React from "react"; import * as DialogPrimitive from "@radix-ui/react-dialog"; import { XIcon } from "lucide-react"; +import * as React from "react"; import { cn } from "@/lib/utils"; @@ -70,7 +70,7 @@ function DialogContent({ } function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { - return
; + return
; } function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { @@ -97,5 +97,6 @@ export { DialogOverlay, DialogPortal, DialogTitle, - DialogTrigger, + DialogTrigger }; +