mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
added and ran prettier autoformatter
This commit is contained in:
parent
e58c2c5c3e
commit
5197268a58
298 changed files with 17336 additions and 20204 deletions
11
ui/litellm-dashboard/.prettierignore
Normal file
11
ui/litellm-dashboard/.prettierignore
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
node_modules
|
||||
.next
|
||||
.out
|
||||
dist
|
||||
build
|
||||
.coverage
|
||||
.vercel
|
||||
.turbo
|
||||
.next-static
|
||||
*.min.js
|
||||
coverage/
|
||||
7
ui/litellm-dashboard/.prettierrc
Normal file
7
ui/litellm-dashboard/.prettierrc
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"semi": true,
|
||||
"singleQuote": false,
|
||||
"tabWidth": 2,
|
||||
"printWidth": 120,
|
||||
"trailingComma": "all"
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"semi": false,
|
||||
"tabWidth": 2,
|
||||
"printWidth": 120,
|
||||
"trailingComma": "all",
|
||||
"jsxBracketSameLine": false
|
||||
}
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
output: 'export',
|
||||
basePath: '',
|
||||
assetPrefix: '/litellm-asset-prefix', // If a server_root_path is set, this will be overridden by runtime injection
|
||||
output: "export",
|
||||
basePath: "",
|
||||
assetPrefix: "/litellm-asset-prefix", // If a server_root_path is set, this will be overridden by runtime injection
|
||||
};
|
||||
|
||||
nextConfig.experimental = {
|
||||
missingSuspenseWithCSRBailout: false
|
||||
}
|
||||
missingSuspenseWithCSRBailout: false,
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
|
|
|||
1
ui/litellm-dashboard/package-lock.json
generated
1
ui/litellm-dashboard/package-lock.json
generated
|
|
@ -17973,6 +17973,7 @@
|
|||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz",
|
||||
"integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -8,7 +8,9 @@
|
|||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"test": "vitest",
|
||||
"test:watch": "vitest -w"
|
||||
"test:watch": "vitest -w",
|
||||
"format": "prettier --write .",
|
||||
"format:check": "prettier --check ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.54.0",
|
||||
|
|
|
|||
|
|
@ -19,12 +19,7 @@
|
|||
|
||||
body {
|
||||
color: rgb(var(--foreground-rgb));
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
transparent,
|
||||
rgb(var(--background-end-rgb))
|
||||
)
|
||||
rgb(var(--background-start-rgb));
|
||||
background: linear-gradient(to bottom, transparent, rgb(var(--background-end-rgb))) rgb(var(--background-start-rgb));
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
|
|
|
|||
|
|
@ -19,7 +19,5 @@ export default function PublicModelHub() {
|
|||
* populate navbar
|
||||
*
|
||||
*/
|
||||
return (
|
||||
<PublicModelHubPage accessToken={accessToken} />
|
||||
);
|
||||
return <PublicModelHubPage accessToken={accessToken} />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,5 @@ export default function PublicModelHubTable() {
|
|||
* populate navbar
|
||||
*
|
||||
*/
|
||||
return (
|
||||
<ModelHubTable accessToken={accessToken} publicPage={true} premiumUser={false} userRole={null}/>
|
||||
);
|
||||
}
|
||||
return <ModelHubTable accessToken={accessToken} publicPage={true} premiumUser={false} userRole={null} />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,7 @@
|
|||
"use client";
|
||||
import React, { Suspense, useEffect, useState } from "react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import {
|
||||
Card,
|
||||
Title,
|
||||
Text,
|
||||
TextInput,
|
||||
Callout,
|
||||
Button,
|
||||
Grid,
|
||||
Col,
|
||||
} from "@tremor/react";
|
||||
import { Card, Title, Text, TextInput, Callout, Button, Grid, Col } from "@tremor/react";
|
||||
import { RiAlarmWarningLine, RiCheckboxCircleLine } from "@remixicon/react";
|
||||
import {
|
||||
invitationClaimCall,
|
||||
|
|
@ -18,7 +9,7 @@ import {
|
|||
getOnboardingCredentials,
|
||||
claimOnboardingToken,
|
||||
getUiConfig,
|
||||
getProxyBaseUrl
|
||||
getProxyBaseUrl,
|
||||
} from "@/components/networking";
|
||||
import { jwtDecode } from "jwt-decode";
|
||||
import { Form, Button as Button2, message } from "antd";
|
||||
|
|
@ -27,7 +18,7 @@ import { getCookie } from "@/utils/cookieUtils";
|
|||
export default function Onboarding() {
|
||||
const [form] = Form.useForm();
|
||||
const searchParams = useSearchParams()!;
|
||||
const token = getCookie('token');
|
||||
const token = getCookie("token");
|
||||
const inviteID = searchParams.get("invitation_id");
|
||||
const action = searchParams.get("action");
|
||||
const [accessToken, setAccessToken] = useState<string | null>(null);
|
||||
|
|
@ -39,14 +30,16 @@ export default function Onboarding() {
|
|||
const [getUiConfigLoading, setGetUiConfigLoading] = useState<boolean>(true);
|
||||
|
||||
useEffect(() => {
|
||||
getUiConfig().then((data) => { // get the information for constructing the proxy base url, and then set the token and auth loading
|
||||
getUiConfig().then((data) => {
|
||||
// get the information for constructing the proxy base url, and then set the token and auth loading
|
||||
console.log("ui config in onboarding.tsx:", data);
|
||||
setGetUiConfigLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!inviteID || getUiConfigLoading) { // wait for the ui config to be loaded
|
||||
if (!inviteID || getUiConfigLoading) {
|
||||
// wait for the ui config to be loaded
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -72,14 +65,7 @@ export default function Onboarding() {
|
|||
}, [inviteID, getUiConfigLoading]);
|
||||
|
||||
const handleSubmit = (formValues: Record<string, any>) => {
|
||||
console.log(
|
||||
"in handle submit. accessToken:",
|
||||
accessToken,
|
||||
"token:",
|
||||
jwtToken,
|
||||
"formValues:",
|
||||
formValues
|
||||
);
|
||||
console.log("in handle submit. accessToken:", accessToken, "token:", jwtToken, "formValues:", formValues);
|
||||
if (!accessToken || !jwtToken) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -89,12 +75,7 @@ export default function Onboarding() {
|
|||
if (!userID || !inviteID) {
|
||||
return;
|
||||
}
|
||||
claimOnboardingToken(
|
||||
accessToken,
|
||||
inviteID,
|
||||
userID,
|
||||
formValues.password
|
||||
).then((data) => {
|
||||
claimOnboardingToken(accessToken, inviteID, userID, formValues.password).then((data) => {
|
||||
let litellm_dashboard_ui = "/ui/";
|
||||
litellm_dashboard_ui += "?login=success";
|
||||
|
||||
|
|
@ -119,15 +100,14 @@ export default function Onboarding() {
|
|||
<Card>
|
||||
<Title className="text-sm mb-5 text-center">🚅 LiteLLM</Title>
|
||||
<Title className="text-xl">{action === "reset_password" ? "Reset Password" : "Sign up"}</Title>
|
||||
<Text>{action === "reset_password" ? "Reset your password to access Admin UI." : "Claim your user account to login to Admin UI."}</Text>
|
||||
<Text>
|
||||
{action === "reset_password"
|
||||
? "Reset your password to access Admin UI."
|
||||
: "Claim your user account to login to Admin UI."}
|
||||
</Text>
|
||||
|
||||
{action !== "reset_password" && (
|
||||
<Callout
|
||||
className="mt-4"
|
||||
title="SSO"
|
||||
icon={RiCheckboxCircleLine}
|
||||
color="sky"
|
||||
>
|
||||
<Callout className="mt-4" title="SSO" icon={RiCheckboxCircleLine} color="sky">
|
||||
<Grid numItems={2} className="flex justify-between items-center">
|
||||
<Col>SSO is under the Enterprise Tier.</Col>
|
||||
|
||||
|
|
@ -142,28 +122,16 @@ export default function Onboarding() {
|
|||
</Callout>
|
||||
)}
|
||||
|
||||
<Form
|
||||
className="mt-10 mb-5 mx-auto"
|
||||
layout="vertical"
|
||||
onFinish={handleSubmit}
|
||||
>
|
||||
<Form className="mt-10 mb-5 mx-auto" layout="vertical" onFinish={handleSubmit}>
|
||||
<>
|
||||
<Form.Item label="Email Address" name="user_email">
|
||||
<TextInput
|
||||
type="email"
|
||||
disabled={true}
|
||||
value={userEmail}
|
||||
defaultValue={userEmail}
|
||||
className="max-w-md"
|
||||
/>
|
||||
<TextInput type="email" disabled={true} value={userEmail} defaultValue={userEmail} className="max-w-md" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="Password"
|
||||
name="password"
|
||||
rules={[
|
||||
{ required: true, message: "password required to sign up" },
|
||||
]}
|
||||
rules={[{ required: true, message: "password required to sign up" }]}
|
||||
help={action === "reset_password" ? "Enter your new password" : "Create a password for your account"}
|
||||
>
|
||||
<TextInput placeholder="" type="password" className="max-w-md" />
|
||||
|
|
|
|||
|
|
@ -1,85 +1,85 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import React, { Suspense, useEffect, useState } from "react"
|
||||
import { useSearchParams } from "next/navigation"
|
||||
import { jwtDecode } from "jwt-decode"
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
|
||||
import { Team } from "@/components/key_team_helpers/key_list"
|
||||
import Navbar from "@/components/navbar"
|
||||
import { ThemeProvider } from "@/contexts/ThemeContext"
|
||||
import UserDashboard from "@/components/user_dashboard"
|
||||
import ModelDashboard from "@/components/templates/model_dashboard"
|
||||
import ViewUserDashboard from "@/components/view_users"
|
||||
import Teams from "@/components/teams"
|
||||
import Organizations from "@/components/organizations"
|
||||
import { fetchOrganizations } from "@/components/organizations"
|
||||
import AdminPanel from "@/components/admins"
|
||||
import Settings from "@/components/settings"
|
||||
import GeneralSettings from "@/components/general_settings"
|
||||
import PassThroughSettings from "@/components/pass_through_settings"
|
||||
import BudgetPanel from "@/components/budgets/budget_panel"
|
||||
import SpendLogsTable from "@/components/view_logs"
|
||||
import ModelHubTable from "@/components/model_hub_table"
|
||||
import NewUsagePage from "@/components/new_usage"
|
||||
import APIRef from "@/components/api_ref"
|
||||
import ChatUI from "@/components/chat_ui/ChatUI"
|
||||
import Sidebar from "@/components/leftnav"
|
||||
import Usage from "@/components/usage"
|
||||
import CacheDashboard from "@/components/cache_dashboard"
|
||||
import { getUiConfig, proxyBaseUrl, setGlobalLitellmHeaderName } from "@/components/networking"
|
||||
import { Organization } from "@/components/networking"
|
||||
import GuardrailsPanel from "@/components/guardrails"
|
||||
import PromptsPanel from "@/components/prompts"
|
||||
import TransformRequestPanel from "@/components/transform_request"
|
||||
import { fetchUserModels } from "@/components/organisms/create_key_button"
|
||||
import { fetchTeams } from "@/components/common_components/fetch_teams"
|
||||
import { MCPServers } from "@/components/mcp_tools"
|
||||
import TagManagement from "@/components/tag_management"
|
||||
import VectorStoreManagement from "@/components/vector_store_management"
|
||||
import UIThemeSettings from "@/components/ui_theme_settings"
|
||||
import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner"
|
||||
import { cx } from "@/lib/cva.config"
|
||||
import React, { Suspense, useEffect, useState } from "react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { jwtDecode } from "jwt-decode";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { Team } from "@/components/key_team_helpers/key_list";
|
||||
import Navbar from "@/components/navbar";
|
||||
import { ThemeProvider } from "@/contexts/ThemeContext";
|
||||
import UserDashboard from "@/components/user_dashboard";
|
||||
import ModelDashboard from "@/components/templates/model_dashboard";
|
||||
import ViewUserDashboard from "@/components/view_users";
|
||||
import Teams from "@/components/teams";
|
||||
import Organizations from "@/components/organizations";
|
||||
import { fetchOrganizations } from "@/components/organizations";
|
||||
import AdminPanel from "@/components/admins";
|
||||
import Settings from "@/components/settings";
|
||||
import GeneralSettings from "@/components/general_settings";
|
||||
import PassThroughSettings from "@/components/pass_through_settings";
|
||||
import BudgetPanel from "@/components/budgets/budget_panel";
|
||||
import SpendLogsTable from "@/components/view_logs";
|
||||
import ModelHubTable from "@/components/model_hub_table";
|
||||
import NewUsagePage from "@/components/new_usage";
|
||||
import APIRef from "@/components/api_ref";
|
||||
import ChatUI from "@/components/chat_ui/ChatUI";
|
||||
import Sidebar from "@/components/leftnav";
|
||||
import Usage from "@/components/usage";
|
||||
import CacheDashboard from "@/components/cache_dashboard";
|
||||
import { getUiConfig, proxyBaseUrl, setGlobalLitellmHeaderName } from "@/components/networking";
|
||||
import { Organization } from "@/components/networking";
|
||||
import GuardrailsPanel from "@/components/guardrails";
|
||||
import PromptsPanel from "@/components/prompts";
|
||||
import TransformRequestPanel from "@/components/transform_request";
|
||||
import { fetchUserModels } from "@/components/organisms/create_key_button";
|
||||
import { fetchTeams } from "@/components/common_components/fetch_teams";
|
||||
import { MCPServers } from "@/components/mcp_tools";
|
||||
import TagManagement from "@/components/tag_management";
|
||||
import VectorStoreManagement from "@/components/vector_store_management";
|
||||
import UIThemeSettings from "@/components/ui_theme_settings";
|
||||
import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner";
|
||||
import { cx } from "@/lib/cva.config";
|
||||
|
||||
function getCookie(name: string) {
|
||||
const cookieValue = document.cookie.split("; ").find((row) => row.startsWith(name + "="))
|
||||
return cookieValue ? cookieValue.split("=")[1] : null
|
||||
const cookieValue = document.cookie.split("; ").find((row) => row.startsWith(name + "="));
|
||||
return cookieValue ? cookieValue.split("=")[1] : null;
|
||||
}
|
||||
|
||||
function formatUserRole(userRole: string) {
|
||||
if (!userRole) {
|
||||
return "Undefined Role"
|
||||
return "Undefined Role";
|
||||
}
|
||||
switch (userRole.toLowerCase()) {
|
||||
case "app_owner":
|
||||
return "App Owner"
|
||||
return "App Owner";
|
||||
case "demo_app_owner":
|
||||
return "App Owner"
|
||||
return "App Owner";
|
||||
case "app_admin":
|
||||
return "Admin"
|
||||
return "Admin";
|
||||
case "proxy_admin":
|
||||
return "Admin"
|
||||
return "Admin";
|
||||
case "proxy_admin_viewer":
|
||||
return "Admin Viewer"
|
||||
return "Admin Viewer";
|
||||
case "org_admin":
|
||||
return "Org Admin"
|
||||
return "Org Admin";
|
||||
case "internal_user":
|
||||
return "Internal User"
|
||||
return "Internal User";
|
||||
case "internal_user_viewer":
|
||||
case "internal_viewer": // TODO:remove if deprecated
|
||||
return "Internal Viewer"
|
||||
return "Internal Viewer";
|
||||
case "app_user":
|
||||
return "App User"
|
||||
return "App User";
|
||||
default:
|
||||
return "Unknown Role"
|
||||
return "Unknown Role";
|
||||
}
|
||||
}
|
||||
|
||||
interface ProxySettings {
|
||||
PROXY_BASE_URL: string
|
||||
PROXY_LOGOUT_URL: string
|
||||
PROXY_BASE_URL: string;
|
||||
PROXY_LOGOUT_URL: string;
|
||||
}
|
||||
|
||||
const queryClient = new QueryClient()
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
function LoadingScreen() {
|
||||
return (
|
||||
|
|
@ -91,51 +91,51 @@ function LoadingScreen() {
|
|||
<span className="text-gray-600 text-sm">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default function CreateKeyPage() {
|
||||
const [userRole, setUserRole] = useState("")
|
||||
const [premiumUser, setPremiumUser] = useState(false)
|
||||
const [disabledPersonalKeyCreation, setDisabledPersonalKeyCreation] = useState(false)
|
||||
const [userEmail, setUserEmail] = useState<null | string>(null)
|
||||
const [teams, setTeams] = useState<Team[] | null>(null)
|
||||
const [keys, setKeys] = useState<null | any[]>([])
|
||||
const [organizations, setOrganizations] = useState<Organization[]>([])
|
||||
const [userModels, setUserModels] = useState<string[]>([])
|
||||
const [userRole, setUserRole] = useState("");
|
||||
const [premiumUser, setPremiumUser] = useState(false);
|
||||
const [disabledPersonalKeyCreation, setDisabledPersonalKeyCreation] = useState(false);
|
||||
const [userEmail, setUserEmail] = useState<null | string>(null);
|
||||
const [teams, setTeams] = useState<Team[] | null>(null);
|
||||
const [keys, setKeys] = useState<null | any[]>([]);
|
||||
const [organizations, setOrganizations] = useState<Organization[]>([]);
|
||||
const [userModels, setUserModels] = useState<string[]>([]);
|
||||
const [proxySettings, setProxySettings] = useState<ProxySettings>({
|
||||
PROXY_BASE_URL: "",
|
||||
PROXY_LOGOUT_URL: "",
|
||||
})
|
||||
});
|
||||
|
||||
const [showSSOBanner, setShowSSOBanner] = useState<boolean>(true)
|
||||
const searchParams = useSearchParams()!
|
||||
const [modelData, setModelData] = useState<any>({ data: [] })
|
||||
const [token, setToken] = useState<string | null>(null)
|
||||
const [createClicked, setCreateClicked] = useState<boolean>(false)
|
||||
const [authLoading, setAuthLoading] = useState(true)
|
||||
const [userID, setUserID] = useState<string | null>(null)
|
||||
const [showSSOBanner, setShowSSOBanner] = useState<boolean>(true);
|
||||
const searchParams = useSearchParams()!;
|
||||
const [modelData, setModelData] = useState<any>({ data: [] });
|
||||
const [token, setToken] = useState<string | null>(null);
|
||||
const [createClicked, setCreateClicked] = useState<boolean>(false);
|
||||
const [authLoading, setAuthLoading] = useState(true);
|
||||
const [userID, setUserID] = useState<string | null>(null);
|
||||
|
||||
const invitation_id = searchParams.get("invitation_id")
|
||||
const invitation_id = searchParams.get("invitation_id");
|
||||
|
||||
// Get page from URL, default to 'api-keys' if not present
|
||||
const [page, setPage] = useState(() => {
|
||||
return searchParams.get("page") || "api-keys"
|
||||
})
|
||||
return searchParams.get("page") || "api-keys";
|
||||
});
|
||||
|
||||
// Custom setPage function that updates URL
|
||||
const updatePage = (newPage: string) => {
|
||||
// Update URL without full page reload
|
||||
const newSearchParams = new URLSearchParams(searchParams)
|
||||
newSearchParams.set("page", newPage)
|
||||
const newSearchParams = new URLSearchParams(searchParams);
|
||||
newSearchParams.set("page", newPage);
|
||||
|
||||
// Use Next.js router to update URL
|
||||
window.history.pushState(null, "", `?${newSearchParams.toString()}`)
|
||||
window.history.pushState(null, "", `?${newSearchParams.toString()}`);
|
||||
|
||||
setPage(newPage)
|
||||
}
|
||||
setPage(newPage);
|
||||
};
|
||||
|
||||
const [accessToken, setAccessToken] = useState<string | null>(null)
|
||||
const [accessToken, setAccessToken] = useState<string | null>(null);
|
||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
||||
|
||||
const toggleSidebar = () => {
|
||||
|
|
@ -143,83 +143,83 @@ export default function CreateKeyPage() {
|
|||
};
|
||||
|
||||
const addKey = (data: any) => {
|
||||
setKeys((prevData) => (prevData ? [...prevData, data] : [data]))
|
||||
setCreateClicked(() => !createClicked)
|
||||
}
|
||||
const redirectToLogin = authLoading === false && token === null && invitation_id === null
|
||||
setKeys((prevData) => (prevData ? [...prevData, data] : [data]));
|
||||
setCreateClicked(() => !createClicked);
|
||||
};
|
||||
const redirectToLogin = authLoading === false && token === null && invitation_id === null;
|
||||
|
||||
useEffect(() => {
|
||||
const token = getCookie("token")
|
||||
const token = getCookie("token");
|
||||
getUiConfig().then((data) => {
|
||||
// get the information for constructing the proxy base url, and then set the token and auth loading
|
||||
setToken(token)
|
||||
setAuthLoading(false)
|
||||
})
|
||||
}, [])
|
||||
setToken(token);
|
||||
setAuthLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (redirectToLogin) {
|
||||
window.location.href = (proxyBaseUrl || "") + "/sso/key/generate"
|
||||
window.location.href = (proxyBaseUrl || "") + "/sso/key/generate";
|
||||
}
|
||||
}, [redirectToLogin])
|
||||
}, [redirectToLogin]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!token) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
const decoded = jwtDecode(token) as { [key: string]: any }
|
||||
const decoded = jwtDecode(token) as { [key: string]: any };
|
||||
if (decoded) {
|
||||
// set accessToken
|
||||
setAccessToken(decoded.key)
|
||||
setAccessToken(decoded.key);
|
||||
|
||||
setDisabledPersonalKeyCreation(decoded.disabled_non_admin_personal_key_creation)
|
||||
setDisabledPersonalKeyCreation(decoded.disabled_non_admin_personal_key_creation);
|
||||
|
||||
// check if userRole is defined
|
||||
if (decoded.user_role) {
|
||||
const formattedUserRole = formatUserRole(decoded.user_role)
|
||||
setUserRole(formattedUserRole)
|
||||
const formattedUserRole = formatUserRole(decoded.user_role);
|
||||
setUserRole(formattedUserRole);
|
||||
if (formattedUserRole == "Admin Viewer") {
|
||||
setPage("usage")
|
||||
setPage("usage");
|
||||
}
|
||||
}
|
||||
|
||||
if (decoded.user_email) {
|
||||
setUserEmail(decoded.user_email)
|
||||
setUserEmail(decoded.user_email);
|
||||
}
|
||||
|
||||
if (decoded.login_method) {
|
||||
setShowSSOBanner(decoded.login_method == "username_password" ? true : false)
|
||||
setShowSSOBanner(decoded.login_method == "username_password" ? true : false);
|
||||
}
|
||||
|
||||
if (decoded.premium_user) {
|
||||
setPremiumUser(decoded.premium_user)
|
||||
setPremiumUser(decoded.premium_user);
|
||||
}
|
||||
|
||||
if (decoded.auth_header_name) {
|
||||
setGlobalLitellmHeaderName(decoded.auth_header_name)
|
||||
setGlobalLitellmHeaderName(decoded.auth_header_name);
|
||||
}
|
||||
|
||||
if (decoded.user_id) {
|
||||
setUserID(decoded.user_id)
|
||||
setUserID(decoded.user_id);
|
||||
}
|
||||
}
|
||||
}, [token])
|
||||
}, [token]);
|
||||
|
||||
useEffect(() => {
|
||||
if (accessToken && userID && userRole) {
|
||||
fetchUserModels(userID, userRole, accessToken, setUserModels)
|
||||
fetchUserModels(userID, userRole, accessToken, setUserModels);
|
||||
}
|
||||
if (accessToken && userID && userRole) {
|
||||
fetchTeams(accessToken, userID, userRole, null, setTeams)
|
||||
fetchTeams(accessToken, userID, userRole, null, setTeams);
|
||||
}
|
||||
if (accessToken) {
|
||||
fetchOrganizations(accessToken, setOrganizations)
|
||||
fetchOrganizations(accessToken, setOrganizations);
|
||||
}
|
||||
}, [accessToken, userID, userRole])
|
||||
}, [accessToken, userID, userRole]);
|
||||
|
||||
if (authLoading || redirectToLogin) {
|
||||
return <LoadingScreen />
|
||||
return <LoadingScreen />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -425,5 +425,5 @@ export default function CreateKeyPage() {
|
|||
</ThemeProvider>
|
||||
</QueryClientProvider>
|
||||
</Suspense>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,24 +1,14 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import {
|
||||
Card,
|
||||
Title,
|
||||
Text,
|
||||
Grid,
|
||||
Col,
|
||||
Button as TremorButton,
|
||||
Callout,
|
||||
TextInput,
|
||||
Divider,
|
||||
} from "@tremor/react";
|
||||
import { Card, Title, Text, Grid, Col, Button as TremorButton, Callout, TextInput, Divider } from "@tremor/react";
|
||||
import { message, Form } from "antd";
|
||||
import { keyCreateCall } from "./networking";
|
||||
import { CopyToClipboard } from "react-copy-to-clipboard";
|
||||
import {
|
||||
LinkOutlined,
|
||||
KeyOutlined,
|
||||
import {
|
||||
LinkOutlined,
|
||||
KeyOutlined,
|
||||
CopyOutlined,
|
||||
ExclamationCircleOutlined,
|
||||
PlusCircleOutlined
|
||||
PlusCircleOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { parseErrorMessage } from "./shared/errorUtils";
|
||||
import NotificationsManager from "./molecules/notifications_manager";
|
||||
|
|
@ -34,38 +24,38 @@ const SCIMConfig: React.FC<SCIMConfigProps> = ({ accessToken, userID, proxySetti
|
|||
const [isCreatingToken, setIsCreatingToken] = useState(false);
|
||||
const [tokenData, setTokenData] = useState<any>(null);
|
||||
const [baseUrl, setBaseUrl] = useState("<your_proxy_base_url>");
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
let url = "<your_proxy_base_url>";
|
||||
|
||||
|
||||
if (proxySettings && proxySettings.PROXY_BASE_URL && proxySettings.PROXY_BASE_URL !== undefined) {
|
||||
url = proxySettings.PROXY_BASE_URL;
|
||||
} else if (typeof window !== 'undefined') {
|
||||
} else if (typeof window !== "undefined") {
|
||||
// Use the current origin as the base URL if no proxy URL is set
|
||||
url = window.location.origin;
|
||||
}
|
||||
|
||||
|
||||
setBaseUrl(url);
|
||||
}, [proxySettings]);
|
||||
|
||||
|
||||
const scimBaseUrl = `${baseUrl}/scim/v2`;
|
||||
|
||||
|
||||
const handleCreateSCIMToken = async (values: any) => {
|
||||
if (!accessToken || !userID) {
|
||||
NotificationsManager.fromBackend("You need to be logged in to create a SCIM token");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
setIsCreatingToken(true);
|
||||
|
||||
|
||||
const formData = {
|
||||
key_alias: values.key_alias || "SCIM Access Token",
|
||||
team_id: null,
|
||||
models: [],
|
||||
allowed_routes: ["/scim/*"],
|
||||
};
|
||||
|
||||
|
||||
const response = await keyCreateCall(accessToken, userID, formData);
|
||||
setTokenData(response);
|
||||
NotificationsManager.success("SCIM token created successfully");
|
||||
|
|
@ -84,11 +74,12 @@ const SCIMConfig: React.FC<SCIMConfigProps> = ({ accessToken, userID, proxySetti
|
|||
<Title>SCIM Configuration</Title>
|
||||
</div>
|
||||
<Text className="text-gray-600">
|
||||
System for Cross-domain Identity Management (SCIM) allows you to automatically provision and manage users and groups in LiteLLM.
|
||||
System for Cross-domain Identity Management (SCIM) allows you to automatically provision and manage users and
|
||||
groups in LiteLLM.
|
||||
</Text>
|
||||
|
||||
|
||||
<Divider />
|
||||
|
||||
|
||||
<div className="space-y-8">
|
||||
{/* Step 1: SCIM URL */}
|
||||
<div>
|
||||
|
|
@ -105,11 +96,7 @@ const SCIMConfig: React.FC<SCIMConfigProps> = ({ accessToken, userID, proxySetti
|
|||
Use this URL in your identity provider SCIM integration settings.
|
||||
</Text>
|
||||
<div className="flex items-center">
|
||||
<TextInput
|
||||
value={scimBaseUrl}
|
||||
disabled={true}
|
||||
className="flex-grow"
|
||||
/>
|
||||
<TextInput value={scimBaseUrl} disabled={true} className="flex-grow" />
|
||||
<CopyToClipboard
|
||||
text={scimBaseUrl}
|
||||
onCopy={() => NotificationsManager.success("URL copied to clipboard")}
|
||||
|
|
@ -133,18 +120,15 @@ const SCIMConfig: React.FC<SCIMConfigProps> = ({ accessToken, userID, proxySetti
|
|||
Authentication Token
|
||||
</Title>
|
||||
</div>
|
||||
|
||||
|
||||
<Callout title="Using SCIM" color="blue" className="mb-4">
|
||||
You need a SCIM token to authenticate with the SCIM API. Create one below and use it in your SCIM provider configuration.
|
||||
You need a SCIM token to authenticate with the SCIM API. Create one below and use it in your SCIM provider
|
||||
configuration.
|
||||
</Callout>
|
||||
|
||||
{!tokenData ? (
|
||||
<div className="bg-gray-50 p-4 rounded-lg">
|
||||
<Form
|
||||
form={form}
|
||||
onFinish={handleCreateSCIMToken}
|
||||
layout="vertical"
|
||||
>
|
||||
<Form form={form} onFinish={handleCreateSCIMToken} layout="vertical">
|
||||
<Form.Item
|
||||
name="key_alias"
|
||||
label="Token Name"
|
||||
|
|
@ -191,11 +175,7 @@ const SCIMConfig: React.FC<SCIMConfigProps> = ({ accessToken, userID, proxySetti
|
|||
</TremorButton>
|
||||
</CopyToClipboard>
|
||||
</div>
|
||||
<TremorButton
|
||||
className="mt-4 flex items-center"
|
||||
variant="secondary"
|
||||
onClick={() => setTokenData(null)}
|
||||
>
|
||||
<TremorButton className="mt-4 flex items-center" variant="secondary" onClick={() => setTokenData(null)}>
|
||||
<PlusCircleOutlined className="h-4 w-4 mr-1" />
|
||||
Create Another Token
|
||||
</TremorButton>
|
||||
|
|
@ -208,4 +188,4 @@ const SCIMConfig: React.FC<SCIMConfigProps> = ({ accessToken, userID, proxySetti
|
|||
);
|
||||
};
|
||||
|
||||
export default SCIMConfig;
|
||||
export default SCIMConfig;
|
||||
|
|
|
|||
|
|
@ -38,56 +38,64 @@ interface SSOProviderConfig {
|
|||
const ssoProviderConfigs: Record<string, SSOProviderConfig> = {
|
||||
google: {
|
||||
envVarMap: {
|
||||
google_client_id: 'GOOGLE_CLIENT_ID',
|
||||
google_client_secret: 'GOOGLE_CLIENT_SECRET',
|
||||
google_client_id: "GOOGLE_CLIENT_ID",
|
||||
google_client_secret: "GOOGLE_CLIENT_SECRET",
|
||||
},
|
||||
fields: [
|
||||
{ label: 'GOOGLE CLIENT ID', name: 'google_client_id' },
|
||||
{ label: 'GOOGLE CLIENT SECRET', name: 'google_client_secret' },
|
||||
{ label: "GOOGLE CLIENT ID", name: "google_client_id" },
|
||||
{ label: "GOOGLE CLIENT SECRET", name: "google_client_secret" },
|
||||
],
|
||||
},
|
||||
microsoft: {
|
||||
envVarMap: {
|
||||
microsoft_client_id: 'MICROSOFT_CLIENT_ID',
|
||||
microsoft_client_secret: 'MICROSOFT_CLIENT_SECRET',
|
||||
microsoft_tenant: 'MICROSOFT_TENANT',
|
||||
microsoft_client_id: "MICROSOFT_CLIENT_ID",
|
||||
microsoft_client_secret: "MICROSOFT_CLIENT_SECRET",
|
||||
microsoft_tenant: "MICROSOFT_TENANT",
|
||||
},
|
||||
fields: [
|
||||
{ label: 'MICROSOFT CLIENT ID', name: 'microsoft_client_id' },
|
||||
{ label: 'MICROSOFT CLIENT SECRET', name: 'microsoft_client_secret' },
|
||||
{ label: 'MICROSOFT TENANT', name: 'microsoft_tenant' },
|
||||
{ label: "MICROSOFT CLIENT ID", name: "microsoft_client_id" },
|
||||
{ label: "MICROSOFT CLIENT SECRET", name: "microsoft_client_secret" },
|
||||
{ label: "MICROSOFT TENANT", name: "microsoft_tenant" },
|
||||
],
|
||||
},
|
||||
okta: {
|
||||
envVarMap: {
|
||||
generic_client_id: 'GENERIC_CLIENT_ID',
|
||||
generic_client_secret: 'GENERIC_CLIENT_SECRET',
|
||||
generic_authorization_endpoint: 'GENERIC_AUTHORIZATION_ENDPOINT',
|
||||
generic_token_endpoint: 'GENERIC_TOKEN_ENDPOINT',
|
||||
generic_userinfo_endpoint: 'GENERIC_USERINFO_ENDPOINT',
|
||||
generic_client_id: "GENERIC_CLIENT_ID",
|
||||
generic_client_secret: "GENERIC_CLIENT_SECRET",
|
||||
generic_authorization_endpoint: "GENERIC_AUTHORIZATION_ENDPOINT",
|
||||
generic_token_endpoint: "GENERIC_TOKEN_ENDPOINT",
|
||||
generic_userinfo_endpoint: "GENERIC_USERINFO_ENDPOINT",
|
||||
},
|
||||
fields: [
|
||||
{ label: 'GENERIC CLIENT ID', name: 'generic_client_id' },
|
||||
{ label: 'GENERIC CLIENT SECRET', name: 'generic_client_secret' },
|
||||
{ label: 'AUTHORIZATION ENDPOINT', name: 'generic_authorization_endpoint', placeholder: 'https://your-okta-domain/authorize' },
|
||||
{ label: 'TOKEN ENDPOINT', name: 'generic_token_endpoint', placeholder: 'https://your-okta-domain/token' },
|
||||
{ label: 'USERINFO ENDPOINT', name: 'generic_userinfo_endpoint', placeholder: 'https://your-okta-domain/userinfo' },
|
||||
{ label: "GENERIC CLIENT ID", name: "generic_client_id" },
|
||||
{ label: "GENERIC CLIENT SECRET", name: "generic_client_secret" },
|
||||
{
|
||||
label: "AUTHORIZATION ENDPOINT",
|
||||
name: "generic_authorization_endpoint",
|
||||
placeholder: "https://your-okta-domain/authorize",
|
||||
},
|
||||
{ label: "TOKEN ENDPOINT", name: "generic_token_endpoint", placeholder: "https://your-okta-domain/token" },
|
||||
{
|
||||
label: "USERINFO ENDPOINT",
|
||||
name: "generic_userinfo_endpoint",
|
||||
placeholder: "https://your-okta-domain/userinfo",
|
||||
},
|
||||
],
|
||||
},
|
||||
generic: {
|
||||
envVarMap: {
|
||||
generic_client_id: 'GENERIC_CLIENT_ID',
|
||||
generic_client_secret: 'GENERIC_CLIENT_SECRET',
|
||||
generic_authorization_endpoint: 'GENERIC_AUTHORIZATION_ENDPOINT',
|
||||
generic_token_endpoint: 'GENERIC_TOKEN_ENDPOINT',
|
||||
generic_userinfo_endpoint: 'GENERIC_USERINFO_ENDPOINT',
|
||||
generic_client_id: "GENERIC_CLIENT_ID",
|
||||
generic_client_secret: "GENERIC_CLIENT_SECRET",
|
||||
generic_authorization_endpoint: "GENERIC_AUTHORIZATION_ENDPOINT",
|
||||
generic_token_endpoint: "GENERIC_TOKEN_ENDPOINT",
|
||||
generic_userinfo_endpoint: "GENERIC_USERINFO_ENDPOINT",
|
||||
},
|
||||
fields: [
|
||||
{ label: 'GENERIC CLIENT ID', name: 'generic_client_id' },
|
||||
{ label: 'GENERIC CLIENT SECRET', name: 'generic_client_secret' },
|
||||
{ label: 'AUTHORIZATION ENDPOINT', name: 'generic_authorization_endpoint' },
|
||||
{ label: 'TOKEN ENDPOINT', name: 'generic_token_endpoint' },
|
||||
{ label: 'USERINFO ENDPOINT', name: 'generic_userinfo_endpoint' },
|
||||
{ label: "GENERIC CLIENT ID", name: "generic_client_id" },
|
||||
{ label: "GENERIC CLIENT SECRET", name: "generic_client_secret" },
|
||||
{ label: "AUTHORIZATION ENDPOINT", name: "generic_authorization_endpoint" },
|
||||
{ label: "TOKEN ENDPOINT", name: "generic_token_endpoint" },
|
||||
{ label: "USERINFO ENDPOINT", name: "generic_userinfo_endpoint" },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
|
@ -116,20 +124,22 @@ const SSOModals: React.FC<SSOModalsProps> = ({
|
|||
if (ssoData && ssoData.values) {
|
||||
console.log("SSO values:", ssoData.values); // Debug log
|
||||
console.log("user_email from API:", ssoData.values.user_email); // Debug log
|
||||
|
||||
|
||||
// Determine which SSO provider is configured
|
||||
let selectedProvider = null;
|
||||
if (ssoData.values.google_client_id) {
|
||||
selectedProvider = 'google';
|
||||
selectedProvider = "google";
|
||||
} else if (ssoData.values.microsoft_client_id) {
|
||||
selectedProvider = 'microsoft';
|
||||
selectedProvider = "microsoft";
|
||||
} else if (ssoData.values.generic_client_id) {
|
||||
// Check if it looks like Okta based on endpoints
|
||||
if (ssoData.values.generic_authorization_endpoint?.includes('okta') ||
|
||||
ssoData.values.generic_authorization_endpoint?.includes('auth0')) {
|
||||
selectedProvider = 'okta';
|
||||
if (
|
||||
ssoData.values.generic_authorization_endpoint?.includes("okta") ||
|
||||
ssoData.values.generic_authorization_endpoint?.includes("auth0")
|
||||
) {
|
||||
selectedProvider = "okta";
|
||||
} else {
|
||||
selectedProvider = 'generic';
|
||||
selectedProvider = "generic";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -142,7 +152,7 @@ const SSOModals: React.FC<SSOModalsProps> = ({
|
|||
};
|
||||
|
||||
console.log("Setting form values:", formValues); // Debug log
|
||||
|
||||
|
||||
// Clear form first, then set values with a small delay to ensure proper initialization
|
||||
form.resetFields();
|
||||
setTimeout(() => {
|
||||
|
|
@ -169,7 +179,7 @@ const SSOModals: React.FC<SSOModalsProps> = ({
|
|||
try {
|
||||
// Save SSO settings using the new API
|
||||
await updateSSOSettings(accessToken, formValues);
|
||||
|
||||
|
||||
// Continue with the original flow (show instructions)
|
||||
handleShowInstructions(formValues);
|
||||
} catch (error) {
|
||||
|
|
@ -204,16 +214,16 @@ const SSOModals: React.FC<SSOModalsProps> = ({
|
|||
};
|
||||
|
||||
await updateSSOSettings(accessToken, clearSettings);
|
||||
|
||||
|
||||
// Clear the form
|
||||
form.resetFields();
|
||||
|
||||
|
||||
// Close the confirmation modal
|
||||
setIsClearConfirmModalVisible(false);
|
||||
|
||||
|
||||
// Close the main SSO modal and trigger refresh
|
||||
handleAddSSOOk();
|
||||
|
||||
|
||||
NotificationsManager.success("SSO settings cleared successfully");
|
||||
} catch (error) {
|
||||
console.error("Failed to clear SSO settings:", error);
|
||||
|
|
@ -233,11 +243,7 @@ const SSOModals: React.FC<SSOModalsProps> = ({
|
|||
name={field.name}
|
||||
rules={[{ required: true, message: `Please enter the ${field.label.toLowerCase()}` }]}
|
||||
>
|
||||
{field.name.includes('client') ? (
|
||||
<Input.Password />
|
||||
) : (
|
||||
<TextInput placeholder={field.placeholder} />
|
||||
)}
|
||||
{field.name.includes("client") ? <Input.Password /> : <TextInput placeholder={field.placeholder} />}
|
||||
</Form.Item>
|
||||
));
|
||||
};
|
||||
|
|
@ -268,8 +274,14 @@ const SSOModals: React.FC<SSOModalsProps> = ({
|
|||
<Select>
|
||||
{Object.entries(ssoProviderLogoMap).map(([value, logo]) => (
|
||||
<Select.Option key={value} value={value}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', padding: '4px 0' }}>
|
||||
{logo && <img src={logo} alt={value} style={{ height: 24, width: 24, marginRight: 12, objectFit: 'contain' }} />}
|
||||
<div style={{ display: "flex", alignItems: "center", padding: "4px 0" }}>
|
||||
{logo && (
|
||||
<img
|
||||
src={logo}
|
||||
alt={value}
|
||||
style={{ height: 24, width: 24, marginRight: 12, objectFit: "contain" }}
|
||||
/>
|
||||
)}
|
||||
<span>{value.charAt(0).toUpperCase() + value.slice(1)} SSO</span>
|
||||
</div>
|
||||
</Select.Option>
|
||||
|
|
@ -282,7 +294,7 @@ const SSOModals: React.FC<SSOModalsProps> = ({
|
|||
shouldUpdate={(prevValues, currentValues) => prevValues.sso_provider !== currentValues.sso_provider}
|
||||
>
|
||||
{({ getFieldValue }) => {
|
||||
const provider = getFieldValue('sso_provider');
|
||||
const provider = getFieldValue("sso_provider");
|
||||
return provider ? renderProviderFields(provider) : null;
|
||||
}}
|
||||
</Form.Item>
|
||||
|
|
@ -302,22 +314,31 @@ const SSOModals: React.FC<SSOModalsProps> = ({
|
|||
<TextInput />
|
||||
</Form.Item>
|
||||
</>
|
||||
<div style={{ textAlign: "right", marginTop: "10px", display: "flex", justifyContent: "flex-end", alignItems: "center", gap: "8px" }}>
|
||||
<div
|
||||
style={{
|
||||
textAlign: "right",
|
||||
marginTop: "10px",
|
||||
display: "flex",
|
||||
justifyContent: "flex-end",
|
||||
alignItems: "center",
|
||||
gap: "8px",
|
||||
}}
|
||||
>
|
||||
{ssoConfigured && (
|
||||
<Button2
|
||||
<Button2
|
||||
onClick={() => setIsClearConfirmModalVisible(true)}
|
||||
style={{
|
||||
backgroundColor: '#6366f1',
|
||||
borderColor: '#6366f1',
|
||||
color: 'white'
|
||||
style={{
|
||||
backgroundColor: "#6366f1",
|
||||
borderColor: "#6366f1",
|
||||
color: "white",
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.backgroundColor = '#5558eb';
|
||||
e.currentTarget.style.borderColor = '#5558eb';
|
||||
e.currentTarget.style.backgroundColor = "#5558eb";
|
||||
e.currentTarget.style.borderColor = "#5558eb";
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.backgroundColor = '#6366f1';
|
||||
e.currentTarget.style.borderColor = '#6366f1';
|
||||
e.currentTarget.style.backgroundColor = "#6366f1";
|
||||
e.currentTarget.style.borderColor = "#6366f1";
|
||||
}}
|
||||
>
|
||||
Clear
|
||||
|
|
@ -336,12 +357,12 @@ const SSOModals: React.FC<SSOModalsProps> = ({
|
|||
onCancel={() => setIsClearConfirmModalVisible(false)}
|
||||
okText="Yes, Clear"
|
||||
cancelText="Cancel"
|
||||
okButtonProps={{
|
||||
okButtonProps={{
|
||||
danger: true,
|
||||
style: {
|
||||
backgroundColor: '#dc2626',
|
||||
borderColor: '#dc2626'
|
||||
}
|
||||
style: {
|
||||
backgroundColor: "#dc2626",
|
||||
borderColor: "#dc2626",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<p>Are you sure you want to clear all SSO settings? This action cannot be undone.</p>
|
||||
|
|
@ -359,13 +380,8 @@ const SSOModals: React.FC<SSOModalsProps> = ({
|
|||
<p>Follow these steps to complete the SSO setup:</p>
|
||||
<Text className="mt-2">1. DO NOT Exit this TAB</Text>
|
||||
<Text className="mt-2">2. Open a new tab, visit your proxy base url</Text>
|
||||
<Text className="mt-2">
|
||||
3. Confirm your SSO is configured correctly and you can login on the new
|
||||
Tab
|
||||
</Text>
|
||||
<Text className="mt-2">
|
||||
4. If Step 3 is successful, you can close this tab
|
||||
</Text>
|
||||
<Text className="mt-2">3. Confirm your SSO is configured correctly and you can login on the new Tab</Text>
|
||||
<Text className="mt-2">4. If Step 3 is successful, you can close this tab</Text>
|
||||
<div style={{ textAlign: "right", marginTop: "10px" }}>
|
||||
<Button2 onClick={handleInstructionsOk}>Done</Button2>
|
||||
</div>
|
||||
|
|
@ -374,5 +390,5 @@ const SSOModals: React.FC<SSOModalsProps> = ({
|
|||
);
|
||||
};
|
||||
|
||||
export { ssoProviderConfigs }; // Export for use in other components
|
||||
export default SSOModals;
|
||||
export { ssoProviderConfigs }; // Export for use in other components
|
||||
export default SSOModals;
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ const SSOSettings: React.FC<SSOSettingsProps> = ({ accessToken, possibleUIRoles,
|
|||
const data = await getInternalUserSettings(accessToken);
|
||||
setSettings(data);
|
||||
setEditedValues(data.values || {});
|
||||
|
||||
|
||||
// Fetch available models
|
||||
if (accessToken) {
|
||||
try {
|
||||
|
|
@ -68,17 +68,20 @@ const SSOSettings: React.FC<SSOSettingsProps> = ({ accessToken, possibleUIRoles,
|
|||
|
||||
const handleSaveSettings = async () => {
|
||||
if (!accessToken) return;
|
||||
|
||||
|
||||
setSaving(true);
|
||||
try {
|
||||
// Convert empty strings to null
|
||||
const processedValues = Object.entries(editedValues).reduce((acc, [key, value]) => {
|
||||
acc[key] = value === "" ? null : value;
|
||||
return acc;
|
||||
}, {} as Record<string, any>);
|
||||
|
||||
const processedValues = Object.entries(editedValues).reduce(
|
||||
(acc, [key, value]) => {
|
||||
acc[key] = value === "" ? null : value;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, any>,
|
||||
);
|
||||
|
||||
const updatedSettings = await updateInternalUserSettings(accessToken, processedValues);
|
||||
setSettings({...settings, values: updatedSettings.settings});
|
||||
setSettings({ ...settings, values: updatedSettings.settings });
|
||||
setIsEditing(false);
|
||||
} catch (error) {
|
||||
console.error("Error updating SSO settings:", error);
|
||||
|
|
@ -91,30 +94,30 @@ const SSOSettings: React.FC<SSOSettingsProps> = ({ accessToken, possibleUIRoles,
|
|||
const handleTextInputChange = (key: string, value: any) => {
|
||||
setEditedValues((prev: Record<string, any>) => ({
|
||||
...prev,
|
||||
[key]: value
|
||||
[key]: value,
|
||||
}));
|
||||
};
|
||||
|
||||
// Helper function to normalize teams array to consistent format
|
||||
const normalizeTeams = (teams: any[]): TeamEntry[] => {
|
||||
if (!teams || !Array.isArray(teams)) return [];
|
||||
|
||||
return teams.map(team => {
|
||||
|
||||
return teams.map((team) => {
|
||||
if (typeof team === "string") {
|
||||
return {
|
||||
team_id: team,
|
||||
user_role: "user" as const
|
||||
user_role: "user" as const,
|
||||
};
|
||||
} else if (typeof team === "object" && team.team_id) {
|
||||
return {
|
||||
team_id: team.team_id,
|
||||
max_budget_in_team: team.max_budget_in_team,
|
||||
user_role: team.user_role || "user"
|
||||
user_role: team.user_role || "user",
|
||||
};
|
||||
}
|
||||
return {
|
||||
team_id: "",
|
||||
user_role: "user" as const
|
||||
user_role: "user" as const,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
|
@ -122,12 +125,12 @@ const SSOSettings: React.FC<SSOSettingsProps> = ({ accessToken, possibleUIRoles,
|
|||
// Teams editor component
|
||||
const renderTeamsEditor = (teams: any[]) => {
|
||||
const normalizedTeams = normalizeTeams(teams);
|
||||
|
||||
|
||||
const updateTeam = (index: number, field: keyof TeamEntry, value: any) => {
|
||||
const updatedTeams = [...normalizedTeams];
|
||||
updatedTeams[index] = {
|
||||
...updatedTeams[index],
|
||||
[field]: value
|
||||
[field]: value,
|
||||
};
|
||||
handleTextInputChange("teams", updatedTeams);
|
||||
};
|
||||
|
|
@ -135,7 +138,7 @@ const SSOSettings: React.FC<SSOSettingsProps> = ({ accessToken, possibleUIRoles,
|
|||
const addTeam = () => {
|
||||
const newTeam: TeamEntry = {
|
||||
team_id: "",
|
||||
user_role: "user"
|
||||
user_role: "user",
|
||||
};
|
||||
handleTextInputChange("teams", [...normalizedTeams, newTeam]);
|
||||
};
|
||||
|
|
@ -161,7 +164,7 @@ const SSOSettings: React.FC<SSOSettingsProps> = ({ accessToken, possibleUIRoles,
|
|||
Remove
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
<div>
|
||||
<Text className="text-sm font-medium mb-1">Team ID</Text>
|
||||
|
|
@ -171,11 +174,11 @@ const SSOSettings: React.FC<SSOSettingsProps> = ({ accessToken, possibleUIRoles,
|
|||
placeholder="Enter team ID"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<Text className="text-sm font-medium mb-1">Max Budget in Team</Text>
|
||||
<InputNumber
|
||||
style={{ width: '100%' }}
|
||||
style={{ width: "100%" }}
|
||||
value={team.max_budget_in_team}
|
||||
onChange={(value) => updateTeam(index, "max_budget_in_team", value)}
|
||||
placeholder="Optional"
|
||||
|
|
@ -184,11 +187,11 @@ const SSOSettings: React.FC<SSOSettingsProps> = ({ accessToken, possibleUIRoles,
|
|||
precision={2}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<Text className="text-sm font-medium mb-1">User Role</Text>
|
||||
<Select
|
||||
style={{ width: '100%' }}
|
||||
style={{ width: "100%" }}
|
||||
value={team.user_role}
|
||||
onChange={(value) => updateTeam(index, "user_role", value)}
|
||||
>
|
||||
|
|
@ -199,13 +202,8 @@ const SSOSettings: React.FC<SSOSettingsProps> = ({ accessToken, possibleUIRoles,
|
|||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<Button
|
||||
variant="secondary"
|
||||
icon={PlusOutlined}
|
||||
onClick={addTeam}
|
||||
className="w-full"
|
||||
>
|
||||
|
||||
<Button variant="secondary" icon={PlusOutlined} onClick={addTeam} className="w-full">
|
||||
Add Team
|
||||
</Button>
|
||||
</div>
|
||||
|
|
@ -214,17 +212,13 @@ const SSOSettings: React.FC<SSOSettingsProps> = ({ accessToken, possibleUIRoles,
|
|||
|
||||
const renderEditableField = (key: string, property: any, value: any) => {
|
||||
const type = property.type;
|
||||
|
||||
|
||||
if (key === "teams") {
|
||||
return (
|
||||
<div className="mt-2">
|
||||
{renderTeamsEditor(editedValues[key] || [])}
|
||||
</div>
|
||||
);
|
||||
return <div className="mt-2">{renderTeamsEditor(editedValues[key] || [])}</div>;
|
||||
} else if (key === "user_role" && possibleUIRoles) {
|
||||
return (
|
||||
<Select
|
||||
style={{ width: '100%' }}
|
||||
style={{ width: "100%" }}
|
||||
value={editedValues[key] || ""}
|
||||
onChange={(value) => handleTextInputChange(key, value)}
|
||||
className="mt-2"
|
||||
|
|
@ -252,23 +246,22 @@ const SSOSettings: React.FC<SSOSettingsProps> = ({ accessToken, possibleUIRoles,
|
|||
} else if (type === "boolean") {
|
||||
return (
|
||||
<div className="mt-2">
|
||||
<Switch
|
||||
checked={!!editedValues[key]}
|
||||
onChange={(checked) => handleTextInputChange(key, checked)}
|
||||
/>
|
||||
<Switch checked={!!editedValues[key]} onChange={(checked) => handleTextInputChange(key, checked)} />
|
||||
</div>
|
||||
);
|
||||
} else if (type === "array" && property.items?.enum) {
|
||||
return (
|
||||
<Select
|
||||
mode="multiple"
|
||||
style={{ width: '100%' }}
|
||||
style={{ width: "100%" }}
|
||||
value={editedValues[key] || []}
|
||||
onChange={(value) => handleTextInputChange(key, value)}
|
||||
className="mt-2"
|
||||
>
|
||||
{property.items.enum.map((option: string) => (
|
||||
<Option key={option} value={option}>{option}</Option>
|
||||
<Option key={option} value={option}>
|
||||
{option}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
|
|
@ -276,7 +269,7 @@ const SSOSettings: React.FC<SSOSettingsProps> = ({ accessToken, possibleUIRoles,
|
|||
return (
|
||||
<Select
|
||||
mode="multiple"
|
||||
style={{ width: '100%' }}
|
||||
style={{ width: "100%" }}
|
||||
value={editedValues[key] || []}
|
||||
onChange={(value) => handleTextInputChange(key, value)}
|
||||
className="mt-2"
|
||||
|
|
@ -292,20 +285,22 @@ const SSOSettings: React.FC<SSOSettingsProps> = ({ accessToken, possibleUIRoles,
|
|||
} else if (type === "string" && property.enum) {
|
||||
return (
|
||||
<Select
|
||||
style={{ width: '100%' }}
|
||||
style={{ width: "100%" }}
|
||||
value={editedValues[key] || ""}
|
||||
onChange={(value) => handleTextInputChange(key, value)}
|
||||
className="mt-2"
|
||||
>
|
||||
{property.enum.map((option: string) => (
|
||||
<Option key={option} value={option}>{option}</Option>
|
||||
<Option key={option} value={option}>
|
||||
{option}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<TextInput
|
||||
value={editedValues[key] !== undefined ? String(editedValues[key]) : ""}
|
||||
<TextInput
|
||||
value={editedValues[key] !== undefined ? String(editedValues[key]) : ""}
|
||||
onChange={(e) => handleTextInputChange(key, e.target.value)}
|
||||
placeholder={property.description || ""}
|
||||
className="mt-2"
|
||||
|
|
@ -316,12 +311,12 @@ const SSOSettings: React.FC<SSOSettingsProps> = ({ accessToken, possibleUIRoles,
|
|||
|
||||
const renderValue = (key: string, value: any): JSX.Element => {
|
||||
if (value === null || value === undefined) return <span className="text-gray-400">Not set</span>;
|
||||
|
||||
|
||||
if (key === "teams" && Array.isArray(value)) {
|
||||
if (value.length === 0) return <span className="text-gray-400">No teams assigned</span>;
|
||||
|
||||
|
||||
const normalizedTeams = normalizeTeams(value);
|
||||
|
||||
|
||||
return (
|
||||
<div className="space-y-2 mt-1">
|
||||
{normalizedTeams.map((team, index) => (
|
||||
|
|
@ -334,8 +329,8 @@ const SSOSettings: React.FC<SSOSettingsProps> = ({ accessToken, possibleUIRoles,
|
|||
<div>
|
||||
<span className="font-medium text-gray-600">Max Budget:</span>
|
||||
<p className="text-gray-900">
|
||||
{team.max_budget_in_team !== undefined
|
||||
? `$${formatNumberWithCommas(team.max_budget_in_team, 4)}`
|
||||
{team.max_budget_in_team !== undefined
|
||||
? `$${formatNumberWithCommas(team.max_budget_in_team, 4)}`
|
||||
: "No limit"}
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -349,7 +344,7 @@ const SSOSettings: React.FC<SSOSettingsProps> = ({ accessToken, possibleUIRoles,
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
if (key === "user_role" && possibleUIRoles && possibleUIRoles[value]) {
|
||||
const { ui_label, description } = possibleUIRoles[value];
|
||||
return (
|
||||
|
|
@ -359,18 +354,18 @@ const SSOSettings: React.FC<SSOSettingsProps> = ({ accessToken, possibleUIRoles,
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
if (key === "budget_duration") {
|
||||
return <span>{getBudgetDurationLabel(value)}</span>;
|
||||
}
|
||||
|
||||
|
||||
if (typeof value === "boolean") {
|
||||
return <span>{value ? "Enabled" : "Disabled"}</span>;
|
||||
}
|
||||
|
||||
|
||||
if (key === "models" && Array.isArray(value)) {
|
||||
if (value.length === 0) return <span className="text-gray-400">None</span>;
|
||||
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap gap-2 mt-1">
|
||||
{value.map((model, index) => (
|
||||
|
|
@ -381,11 +376,11 @@ const SSOSettings: React.FC<SSOSettingsProps> = ({ accessToken, possibleUIRoles,
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
if (typeof value === "object") {
|
||||
if (Array.isArray(value)) {
|
||||
if (value.length === 0) return <span className="text-gray-400">None</span>;
|
||||
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap gap-2 mt-1">
|
||||
{value.map((item, index) => (
|
||||
|
|
@ -396,14 +391,10 @@ const SSOSettings: React.FC<SSOSettingsProps> = ({ accessToken, possibleUIRoles,
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<pre className="bg-gray-100 p-2 rounded text-xs overflow-auto mt-1">
|
||||
{JSON.stringify(value, null, 2)}
|
||||
</pre>
|
||||
);
|
||||
|
||||
return <pre className="bg-gray-100 p-2 rounded text-xs overflow-auto mt-1">{JSON.stringify(value, null, 2)}</pre>;
|
||||
}
|
||||
|
||||
|
||||
return <span>{String(value)}</span>;
|
||||
};
|
||||
|
||||
|
|
@ -426,30 +417,26 @@ const SSOSettings: React.FC<SSOSettingsProps> = ({ accessToken, possibleUIRoles,
|
|||
// Dynamically render settings based on the schema
|
||||
const renderSettings = () => {
|
||||
const { values, field_schema } = settings;
|
||||
|
||||
|
||||
if (!field_schema || !field_schema.properties) {
|
||||
return <Text>No schema information available</Text>;
|
||||
}
|
||||
|
||||
return Object.entries(field_schema.properties).map(([key, property]: [string, any]) => {
|
||||
const value = values[key];
|
||||
const displayName = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
|
||||
|
||||
const displayName = key.replace(/_/g, " ").replace(/\b\w/g, (l) => l.toUpperCase());
|
||||
|
||||
return (
|
||||
<div key={key} className="mb-6 pb-6 border-b border-gray-200 last:border-0">
|
||||
<Text className="font-medium text-lg">{displayName}</Text>
|
||||
<Paragraph className="text-sm text-gray-500 mt-1">
|
||||
{property.description || "No description available"}
|
||||
</Paragraph>
|
||||
|
||||
|
||||
{isEditing ? (
|
||||
<div className="mt-2">
|
||||
{renderEditableField(key, property, value)}
|
||||
</div>
|
||||
<div className="mt-2">{renderEditableField(key, property, value)}</div>
|
||||
) : (
|
||||
<div className="mt-1 p-2 bg-gray-50 rounded">
|
||||
{renderValue(key, value)}
|
||||
</div>
|
||||
<div className="mt-1 p-2 bg-gray-50 rounded">{renderValue(key, value)}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
|
@ -460,10 +447,11 @@ const SSOSettings: React.FC<SSOSettingsProps> = ({ accessToken, possibleUIRoles,
|
|||
<Card>
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<Title>Default User Settings</Title>
|
||||
{!loading && settings && (
|
||||
isEditing ? (
|
||||
{!loading &&
|
||||
settings &&
|
||||
(isEditing ? (
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
setIsEditing(false);
|
||||
|
|
@ -473,33 +461,23 @@ const SSOSettings: React.FC<SSOSettingsProps> = ({ accessToken, possibleUIRoles,
|
|||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSaveSettings}
|
||||
loading={saving}
|
||||
>
|
||||
<Button onClick={handleSaveSettings} loading={saving}>
|
||||
Save Changes
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
onClick={() => setIsEditing(true)}
|
||||
>
|
||||
Edit Settings
|
||||
</Button>
|
||||
)
|
||||
)}
|
||||
<Button onClick={() => setIsEditing(true)}>Edit Settings</Button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
||||
{settings?.field_schema?.description && (
|
||||
<Paragraph className="mb-4">{settings.field_schema.description}</Paragraph>
|
||||
)}
|
||||
<Divider />
|
||||
|
||||
<div className="mt-4 space-y-4">
|
||||
{renderSettings()}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 space-y-4">{renderSettings()}</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default SSOSettings;
|
||||
export default SSOSettings;
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ const TeamSSOSettings: React.FC<TeamSSOSettingsProps> = ({ accessToken, userID,
|
|||
const data = await getDefaultTeamSettings(accessToken);
|
||||
setSettings(data);
|
||||
setEditedValues(data.values || {});
|
||||
|
||||
|
||||
// Fetch available models
|
||||
if (accessToken) {
|
||||
try {
|
||||
|
|
@ -59,11 +59,11 @@ const TeamSSOSettings: React.FC<TeamSSOSettingsProps> = ({ accessToken, userID,
|
|||
|
||||
const handleSaveSettings = async () => {
|
||||
if (!accessToken) return;
|
||||
|
||||
|
||||
setSaving(true);
|
||||
try {
|
||||
const updatedSettings = await updateDefaultTeamSettings(accessToken, editedValues);
|
||||
setSettings({...settings, values: updatedSettings.settings});
|
||||
setSettings({ ...settings, values: updatedSettings.settings });
|
||||
setIsEditing(false);
|
||||
NotificationsManager.success("Default team settings updated successfully");
|
||||
} catch (error) {
|
||||
|
|
@ -77,13 +77,13 @@ const TeamSSOSettings: React.FC<TeamSSOSettingsProps> = ({ accessToken, userID,
|
|||
const handleTextInputChange = (key: string, value: any) => {
|
||||
setEditedValues((prev: Record<string, any>) => ({
|
||||
...prev,
|
||||
[key]: value
|
||||
[key]: value,
|
||||
}));
|
||||
};
|
||||
|
||||
const renderEditableField = (key: string, property: any, value: any) => {
|
||||
const type = property.type;
|
||||
|
||||
|
||||
if (key === "budget_duration") {
|
||||
return (
|
||||
<BudgetDurationDropdown
|
||||
|
|
@ -95,23 +95,22 @@ const TeamSSOSettings: React.FC<TeamSSOSettingsProps> = ({ accessToken, userID,
|
|||
} else if (type === "boolean") {
|
||||
return (
|
||||
<div className="mt-2">
|
||||
<Switch
|
||||
checked={!!editedValues[key]}
|
||||
onChange={(checked) => handleTextInputChange(key, checked)}
|
||||
/>
|
||||
<Switch checked={!!editedValues[key]} onChange={(checked) => handleTextInputChange(key, checked)} />
|
||||
</div>
|
||||
);
|
||||
} else if (type === "array" && property.items?.enum) {
|
||||
return (
|
||||
<Select
|
||||
mode="multiple"
|
||||
style={{ width: '100%' }}
|
||||
style={{ width: "100%" }}
|
||||
value={editedValues[key] || []}
|
||||
onChange={(value) => handleTextInputChange(key, value)}
|
||||
className="mt-2"
|
||||
>
|
||||
{property.items.enum.map((option: string) => (
|
||||
<Option key={option} value={option}>{option}</Option>
|
||||
<Option key={option} value={option}>
|
||||
{option}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
|
|
@ -119,7 +118,7 @@ const TeamSSOSettings: React.FC<TeamSSOSettingsProps> = ({ accessToken, userID,
|
|||
return (
|
||||
<Select
|
||||
mode="multiple"
|
||||
style={{ width: '100%' }}
|
||||
style={{ width: "100%" }}
|
||||
value={editedValues[key] || []}
|
||||
onChange={(value) => handleTextInputChange(key, value)}
|
||||
className="mt-2"
|
||||
|
|
@ -134,20 +133,22 @@ const TeamSSOSettings: React.FC<TeamSSOSettingsProps> = ({ accessToken, userID,
|
|||
} else if (type === "string" && property.enum) {
|
||||
return (
|
||||
<Select
|
||||
style={{ width: '100%' }}
|
||||
style={{ width: "100%" }}
|
||||
value={editedValues[key] || ""}
|
||||
onChange={(value) => handleTextInputChange(key, value)}
|
||||
className="mt-2"
|
||||
>
|
||||
{property.enum.map((option: string) => (
|
||||
<Option key={option} value={option}>{option}</Option>
|
||||
<Option key={option} value={option}>
|
||||
{option}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<TextInput
|
||||
value={editedValues[key] !== undefined ? String(editedValues[key]) : ""}
|
||||
<TextInput
|
||||
value={editedValues[key] !== undefined ? String(editedValues[key]) : ""}
|
||||
onChange={(e) => handleTextInputChange(key, e.target.value)}
|
||||
placeholder={property.description || ""}
|
||||
className="mt-2"
|
||||
|
|
@ -158,18 +159,18 @@ const TeamSSOSettings: React.FC<TeamSSOSettingsProps> = ({ accessToken, userID,
|
|||
|
||||
const renderValue = (key: string, value: any): JSX.Element => {
|
||||
if (value === null || value === undefined) return <span className="text-gray-400">Not set</span>;
|
||||
|
||||
|
||||
if (key === "budget_duration") {
|
||||
return <span>{getBudgetDurationLabel(value)}</span>;
|
||||
}
|
||||
|
||||
|
||||
if (typeof value === "boolean") {
|
||||
return <span>{value ? "Enabled" : "Disabled"}</span>;
|
||||
}
|
||||
|
||||
|
||||
if (key === "models" && Array.isArray(value)) {
|
||||
if (value.length === 0) return <span className="text-gray-400">None</span>;
|
||||
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap gap-2 mt-1">
|
||||
{value.map((model, index) => (
|
||||
|
|
@ -180,11 +181,11 @@ const TeamSSOSettings: React.FC<TeamSSOSettingsProps> = ({ accessToken, userID,
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
if (typeof value === "object") {
|
||||
if (Array.isArray(value)) {
|
||||
if (value.length === 0) return <span className="text-gray-400">None</span>;
|
||||
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap gap-2 mt-1">
|
||||
{value.map((item, index) => (
|
||||
|
|
@ -195,14 +196,10 @@ const TeamSSOSettings: React.FC<TeamSSOSettingsProps> = ({ accessToken, userID,
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<pre className="bg-gray-100 p-2 rounded text-xs overflow-auto mt-1">
|
||||
{JSON.stringify(value, null, 2)}
|
||||
</pre>
|
||||
);
|
||||
|
||||
return <pre className="bg-gray-100 p-2 rounded text-xs overflow-auto mt-1">{JSON.stringify(value, null, 2)}</pre>;
|
||||
}
|
||||
|
||||
|
||||
return <span>{String(value)}</span>;
|
||||
};
|
||||
|
||||
|
|
@ -225,30 +222,26 @@ const TeamSSOSettings: React.FC<TeamSSOSettingsProps> = ({ accessToken, userID,
|
|||
// Dynamically render settings based on the schema
|
||||
const renderSettings = () => {
|
||||
const { values, field_schema } = settings;
|
||||
|
||||
|
||||
if (!field_schema || !field_schema.properties) {
|
||||
return <Text>No schema information available</Text>;
|
||||
}
|
||||
|
||||
return Object.entries(field_schema.properties).map(([key, property]: [string, any]) => {
|
||||
const value = values[key];
|
||||
const displayName = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
|
||||
|
||||
const displayName = key.replace(/_/g, " ").replace(/\b\w/g, (l) => l.toUpperCase());
|
||||
|
||||
return (
|
||||
<div key={key} className="mb-6 pb-6 border-b border-gray-200 last:border-0">
|
||||
<Text className="font-medium text-lg">{displayName}</Text>
|
||||
<Paragraph className="text-sm text-gray-500 mt-1">
|
||||
{property.description || "No description available"}
|
||||
</Paragraph>
|
||||
|
||||
|
||||
{isEditing ? (
|
||||
<div className="mt-2">
|
||||
{renderEditableField(key, property, value)}
|
||||
</div>
|
||||
<div className="mt-2">{renderEditableField(key, property, value)}</div>
|
||||
) : (
|
||||
<div className="mt-1 p-2 bg-gray-50 rounded">
|
||||
{renderValue(key, value)}
|
||||
</div>
|
||||
<div className="mt-1 p-2 bg-gray-50 rounded">{renderValue(key, value)}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
|
@ -259,10 +252,11 @@ const TeamSSOSettings: React.FC<TeamSSOSettingsProps> = ({ accessToken, userID,
|
|||
<Card>
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<Title className="text-xl">Default Team Settings</Title>
|
||||
{!loading && settings && (
|
||||
isEditing ? (
|
||||
{!loading &&
|
||||
settings &&
|
||||
(isEditing ? (
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
setIsEditing(false);
|
||||
|
|
@ -272,37 +266,25 @@ const TeamSSOSettings: React.FC<TeamSSOSettingsProps> = ({ accessToken, userID,
|
|||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSaveSettings}
|
||||
loading={saving}
|
||||
>
|
||||
<Button onClick={handleSaveSettings} loading={saving}>
|
||||
Save Changes
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
onClick={() => setIsEditing(true)}
|
||||
>
|
||||
Edit Settings
|
||||
</Button>
|
||||
)
|
||||
)}
|
||||
<Button onClick={() => setIsEditing(true)}>Edit Settings</Button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Text>
|
||||
These settings will be applied by default when creating new teams.
|
||||
</Text>
|
||||
|
||||
|
||||
<Text>These settings will be applied by default when creating new teams.</Text>
|
||||
|
||||
{settings?.field_schema?.description && (
|
||||
<Paragraph className="mb-4 mt-2">{settings.field_schema.description}</Paragraph>
|
||||
)}
|
||||
<Divider />
|
||||
|
||||
<div className="mt-4 space-y-4">
|
||||
{renderSettings()}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 space-y-4">{renderSettings()}</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default TeamSSOSettings;
|
||||
export default TeamSSOSettings;
|
||||
|
|
|
|||
|
|
@ -24,14 +24,14 @@ const UIAccessControlForm: React.FC<UIAccessControlFormProps> = ({ accessToken,
|
|||
// Handle nested ui_access_mode structure
|
||||
const uiAccessMode = ssoData.values.ui_access_mode;
|
||||
let formValues = {};
|
||||
|
||||
if (uiAccessMode && typeof uiAccessMode === 'object') {
|
||||
|
||||
if (uiAccessMode && typeof uiAccessMode === "object") {
|
||||
formValues = {
|
||||
ui_access_mode_type: uiAccessMode.type,
|
||||
restricted_sso_group: uiAccessMode.restricted_sso_group,
|
||||
sso_group_jwt_field: uiAccessMode.sso_group_jwt_field,
|
||||
};
|
||||
} else if (typeof uiAccessMode === 'string') {
|
||||
} else if (typeof uiAccessMode === "string") {
|
||||
// Handle legacy flat structure
|
||||
formValues = {
|
||||
ui_access_mode_type: uiAccessMode,
|
||||
|
|
@ -39,7 +39,7 @@ const UIAccessControlForm: React.FC<UIAccessControlFormProps> = ({ accessToken,
|
|||
sso_group_jwt_field: ssoData.values.team_ids_jwt_field || ssoData.values.sso_group_jwt_field,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
form.setFieldsValue(formValues);
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
@ -61,11 +61,11 @@ const UIAccessControlForm: React.FC<UIAccessControlFormProps> = ({ accessToken,
|
|||
try {
|
||||
// Transform form data to match API expected structure
|
||||
let apiPayload;
|
||||
|
||||
if (formValues.ui_access_mode_type === 'all_authenticated_users') {
|
||||
|
||||
if (formValues.ui_access_mode_type === "all_authenticated_users") {
|
||||
// Set ui_access_mode to none when all_authenticated_users is selected
|
||||
apiPayload = {
|
||||
ui_access_mode: "none"
|
||||
ui_access_mode: "none",
|
||||
};
|
||||
} else {
|
||||
apiPayload = {
|
||||
|
|
@ -73,7 +73,7 @@ const UIAccessControlForm: React.FC<UIAccessControlFormProps> = ({ accessToken,
|
|||
type: formValues.ui_access_mode_type,
|
||||
restricted_sso_group: formValues.restricted_sso_group,
|
||||
sso_group_jwt_field: formValues.sso_group_jwt_field,
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -88,23 +88,15 @@ const UIAccessControlForm: React.FC<UIAccessControlFormProps> = ({ accessToken,
|
|||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: '16px' }}>
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<Text style={{ fontSize: '14px', color: '#6b7280' }}>
|
||||
<div style={{ padding: "16px" }}>
|
||||
<div style={{ marginBottom: "16px" }}>
|
||||
<Text style={{ fontSize: "14px", color: "#6b7280" }}>
|
||||
Configure who can access the UI interface and how group information is extracted from JWT tokens.
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<Form
|
||||
form={form}
|
||||
onFinish={handleUIAccessSubmit}
|
||||
layout="vertical"
|
||||
>
|
||||
<Form.Item
|
||||
label="UI Access Mode"
|
||||
name="ui_access_mode_type"
|
||||
tooltip="Controls who can access the UI interface"
|
||||
>
|
||||
|
||||
<Form form={form} onFinish={handleUIAccessSubmit} layout="vertical">
|
||||
<Form.Item label="UI Access Mode" name="ui_access_mode_type" tooltip="Controls who can access the UI interface">
|
||||
<Select placeholder="Select access mode">
|
||||
<Select.Option value="all_authenticated_users">All Authenticated Users</Select.Option>
|
||||
<Select.Option value="restricted_sso_group">Restricted SSO Group</Select.Option>
|
||||
|
|
@ -113,11 +105,13 @@ const UIAccessControlForm: React.FC<UIAccessControlFormProps> = ({ accessToken,
|
|||
|
||||
<Form.Item
|
||||
noStyle
|
||||
shouldUpdate={(prevValues, currentValues) => prevValues.ui_access_mode_type !== currentValues.ui_access_mode_type}
|
||||
shouldUpdate={(prevValues, currentValues) =>
|
||||
prevValues.ui_access_mode_type !== currentValues.ui_access_mode_type
|
||||
}
|
||||
>
|
||||
{({ getFieldValue }) => {
|
||||
const uiAccessModeType = getFieldValue('ui_access_mode_type');
|
||||
return uiAccessModeType === 'restricted_sso_group' ? (
|
||||
const uiAccessModeType = getFieldValue("ui_access_mode_type");
|
||||
return uiAccessModeType === "restricted_sso_group" ? (
|
||||
<Form.Item
|
||||
label="Restricted SSO Group"
|
||||
name="restricted_sso_group"
|
||||
|
|
@ -138,13 +132,13 @@ const UIAccessControlForm: React.FC<UIAccessControlFormProps> = ({ accessToken,
|
|||
</Form.Item>
|
||||
|
||||
<div style={{ textAlign: "right", marginTop: "16px" }}>
|
||||
<Button2
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
<Button2
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
loading={loading}
|
||||
style={{
|
||||
backgroundColor: '#6366f1',
|
||||
borderColor: '#6366f1'
|
||||
style={{
|
||||
backgroundColor: "#6366f1",
|
||||
borderColor: "#6366f1",
|
||||
}}
|
||||
>
|
||||
Update UI Access Control
|
||||
|
|
@ -155,4 +149,4 @@ const UIAccessControlForm: React.FC<UIAccessControlFormProps> = ({ accessToken,
|
|||
);
|
||||
};
|
||||
|
||||
export default UIAccessControlForm;
|
||||
export default UIAccessControlForm;
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import * as networking from './networking';
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import * as networking from "./networking";
|
||||
|
||||
// Mock the networking module
|
||||
vi.mock('./networking', () => ({
|
||||
vi.mock("./networking", () => ({
|
||||
updateSSOSettings: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock NotificationManager
|
||||
vi.mock('./molecules/notifications_manager', () => ({
|
||||
vi.mock("./molecules/notifications_manager", () => ({
|
||||
default: {
|
||||
fromBackend: vi.fn(),
|
||||
},
|
||||
|
|
@ -15,10 +15,10 @@ vi.mock('./molecules/notifications_manager', () => ({
|
|||
|
||||
// Extract the logic we want to test into a pure function
|
||||
const buildApiPayload = (formValues: Record<string, any>) => {
|
||||
if (formValues.ui_access_mode_type === 'all_authenticated_users') {
|
||||
if (formValues.ui_access_mode_type === "all_authenticated_users") {
|
||||
// Set ui_access_mode to none when all_authenticated_users is selected
|
||||
return {
|
||||
ui_access_mode: "none"
|
||||
ui_access_mode: "none",
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
|
|
@ -26,70 +26,70 @@ const buildApiPayload = (formValues: Record<string, any>) => {
|
|||
type: formValues.ui_access_mode_type,
|
||||
restricted_sso_group: formValues.restricted_sso_group,
|
||||
sso_group_jwt_field: formValues.sso_group_jwt_field,
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
describe('UIAccessControlForm Logic', () => {
|
||||
describe("UIAccessControlForm Logic", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
describe('buildApiPayload', () => {
|
||||
describe("buildApiPayload", () => {
|
||||
it('should return ui_access_mode="none" when ui_access_mode_type is "all_authenticated_users"', () => {
|
||||
const formValues = {
|
||||
ui_access_mode_type: 'all_authenticated_users',
|
||||
restricted_sso_group: 'some-group',
|
||||
sso_group_jwt_field: 'groups',
|
||||
ui_access_mode_type: "all_authenticated_users",
|
||||
restricted_sso_group: "some-group",
|
||||
sso_group_jwt_field: "groups",
|
||||
};
|
||||
|
||||
const result = buildApiPayload(formValues);
|
||||
|
||||
expect(result).toEqual({
|
||||
ui_access_mode: "none"
|
||||
ui_access_mode: "none",
|
||||
});
|
||||
});
|
||||
|
||||
it('should return object structure when ui_access_mode_type is "restricted_sso_group"', () => {
|
||||
const formValues = {
|
||||
ui_access_mode_type: 'restricted_sso_group',
|
||||
restricted_sso_group: 'admin-users',
|
||||
sso_group_jwt_field: 'team_groups',
|
||||
ui_access_mode_type: "restricted_sso_group",
|
||||
restricted_sso_group: "admin-users",
|
||||
sso_group_jwt_field: "team_groups",
|
||||
};
|
||||
|
||||
const result = buildApiPayload(formValues);
|
||||
|
||||
expect(result).toEqual({
|
||||
ui_access_mode: {
|
||||
type: 'restricted_sso_group',
|
||||
restricted_sso_group: 'admin-users',
|
||||
sso_group_jwt_field: 'team_groups',
|
||||
}
|
||||
type: "restricted_sso_group",
|
||||
restricted_sso_group: "admin-users",
|
||||
sso_group_jwt_field: "team_groups",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should return object structure for any other ui_access_mode_type', () => {
|
||||
it("should return object structure for any other ui_access_mode_type", () => {
|
||||
const formValues = {
|
||||
ui_access_mode_type: 'some_other_mode',
|
||||
restricted_sso_group: 'test-group',
|
||||
sso_group_jwt_field: 'user_groups',
|
||||
ui_access_mode_type: "some_other_mode",
|
||||
restricted_sso_group: "test-group",
|
||||
sso_group_jwt_field: "user_groups",
|
||||
};
|
||||
|
||||
const result = buildApiPayload(formValues);
|
||||
|
||||
expect(result).toEqual({
|
||||
ui_access_mode: {
|
||||
type: 'some_other_mode',
|
||||
restricted_sso_group: 'test-group',
|
||||
sso_group_jwt_field: 'user_groups',
|
||||
}
|
||||
type: "some_other_mode",
|
||||
restricted_sso_group: "test-group",
|
||||
sso_group_jwt_field: "user_groups",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle undefined values gracefully', () => {
|
||||
it("should handle undefined values gracefully", () => {
|
||||
const formValues = {
|
||||
ui_access_mode_type: 'restricted_sso_group',
|
||||
ui_access_mode_type: "restricted_sso_group",
|
||||
restricted_sso_group: undefined,
|
||||
sso_group_jwt_field: undefined,
|
||||
};
|
||||
|
|
@ -98,79 +98,73 @@ describe('UIAccessControlForm Logic', () => {
|
|||
|
||||
expect(result).toEqual({
|
||||
ui_access_mode: {
|
||||
type: 'restricted_sso_group',
|
||||
type: "restricted_sso_group",
|
||||
restricted_sso_group: undefined,
|
||||
sso_group_jwt_field: undefined,
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should prioritize all_authenticated_users over other values', () => {
|
||||
it("should prioritize all_authenticated_users over other values", () => {
|
||||
const formValues = {
|
||||
ui_access_mode_type: 'all_authenticated_users',
|
||||
restricted_sso_group: 'admin-group',
|
||||
sso_group_jwt_field: 'groups',
|
||||
ui_access_mode_type: "all_authenticated_users",
|
||||
restricted_sso_group: "admin-group",
|
||||
sso_group_jwt_field: "groups",
|
||||
};
|
||||
|
||||
const result = buildApiPayload(formValues);
|
||||
|
||||
// Should return "none" and ignore the other fields
|
||||
expect(result).toEqual({
|
||||
ui_access_mode: "none"
|
||||
ui_access_mode: "none",
|
||||
});
|
||||
|
||||
|
||||
// Verify other fields are not included
|
||||
expect(result).not.toHaveProperty('restricted_sso_group');
|
||||
expect(result).not.toHaveProperty('sso_group_jwt_field');
|
||||
expect(result).not.toHaveProperty("restricted_sso_group");
|
||||
expect(result).not.toHaveProperty("sso_group_jwt_field");
|
||||
});
|
||||
});
|
||||
|
||||
describe('API Integration', () => {
|
||||
it('should call updateSSOSettings with correct payload for all_authenticated_users', async () => {
|
||||
const mockAccessToken = 'test-token';
|
||||
describe("API Integration", () => {
|
||||
it("should call updateSSOSettings with correct payload for all_authenticated_users", async () => {
|
||||
const mockAccessToken = "test-token";
|
||||
const formValues = {
|
||||
ui_access_mode_type: 'all_authenticated_users',
|
||||
sso_group_jwt_field: 'groups',
|
||||
ui_access_mode_type: "all_authenticated_users",
|
||||
sso_group_jwt_field: "groups",
|
||||
};
|
||||
|
||||
vi.mocked(networking.updateSSOSettings).mockResolvedValue({});
|
||||
|
||||
const expectedPayload = buildApiPayload(formValues);
|
||||
|
||||
|
||||
// Simulate the API call
|
||||
await networking.updateSSOSettings(mockAccessToken, expectedPayload);
|
||||
|
||||
expect(networking.updateSSOSettings).toHaveBeenCalledWith(
|
||||
mockAccessToken,
|
||||
{ ui_access_mode: "none" }
|
||||
);
|
||||
expect(networking.updateSSOSettings).toHaveBeenCalledWith(mockAccessToken, { ui_access_mode: "none" });
|
||||
});
|
||||
|
||||
it('should call updateSSOSettings with correct payload for restricted_sso_group', async () => {
|
||||
const mockAccessToken = 'test-token';
|
||||
it("should call updateSSOSettings with correct payload for restricted_sso_group", async () => {
|
||||
const mockAccessToken = "test-token";
|
||||
const formValues = {
|
||||
ui_access_mode_type: 'restricted_sso_group',
|
||||
restricted_sso_group: 'admin-team',
|
||||
sso_group_jwt_field: 'team_groups',
|
||||
ui_access_mode_type: "restricted_sso_group",
|
||||
restricted_sso_group: "admin-team",
|
||||
sso_group_jwt_field: "team_groups",
|
||||
};
|
||||
|
||||
vi.mocked(networking.updateSSOSettings).mockResolvedValue({});
|
||||
|
||||
const expectedPayload = buildApiPayload(formValues);
|
||||
|
||||
|
||||
// Simulate the API call
|
||||
await networking.updateSSOSettings(mockAccessToken, expectedPayload);
|
||||
|
||||
expect(networking.updateSSOSettings).toHaveBeenCalledWith(
|
||||
mockAccessToken,
|
||||
{
|
||||
ui_access_mode: {
|
||||
type: 'restricted_sso_group',
|
||||
restricted_sso_group: 'admin-team',
|
||||
sso_group_jwt_field: 'team_groups',
|
||||
}
|
||||
}
|
||||
);
|
||||
expect(networking.updateSSOSettings).toHaveBeenCalledWith(mockAccessToken, {
|
||||
ui_access_mode: {
|
||||
type: "restricted_sso_group",
|
||||
restricted_sso_group: "admin-team",
|
||||
sso_group_jwt_field: "team_groups",
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,23 +1,17 @@
|
|||
import React from 'react';
|
||||
import { Card, Grid, Text, Title } from '@tremor/react';
|
||||
import { AreaChart, BarChart } from '@tremor/react';
|
||||
import { DailyData, ModelActivityData, KeyMetricWithMetadata, TopApiKeyData } from './usage/types';
|
||||
import { Collapse } from 'antd';
|
||||
import { formatNumberWithCommas } from '@/utils/dataUtils';
|
||||
import { valueFormatter } from '../components/usage/utils/value_formatters';
|
||||
import { CustomTooltip, CustomLegend } from './common_components/chartUtils';
|
||||
import React from "react";
|
||||
import { Card, Grid, Text, Title } from "@tremor/react";
|
||||
import { AreaChart, BarChart } from "@tremor/react";
|
||||
import { DailyData, ModelActivityData, KeyMetricWithMetadata, TopApiKeyData } from "./usage/types";
|
||||
import { Collapse } from "antd";
|
||||
import { formatNumberWithCommas } from "@/utils/dataUtils";
|
||||
import { valueFormatter } from "../components/usage/utils/value_formatters";
|
||||
import { CustomTooltip, CustomLegend } from "./common_components/chartUtils";
|
||||
|
||||
interface ActivityMetricsProps {
|
||||
modelMetrics: Record<string, ModelActivityData>;
|
||||
}
|
||||
|
||||
const ModelSection = ({
|
||||
modelName,
|
||||
metrics,
|
||||
}: {
|
||||
modelName: string;
|
||||
metrics: ModelActivityData;
|
||||
}) => {
|
||||
const ModelSection = ({ modelName, metrics }: { modelName: string; metrics: ModelActivityData }) => {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{/* Summary Cards */}
|
||||
|
|
@ -33,23 +27,13 @@ const ModelSection = ({
|
|||
<Card>
|
||||
<Text>Total Tokens</Text>
|
||||
<Title>{metrics.total_tokens.toLocaleString()}</Title>
|
||||
<Text>
|
||||
{Math.round(
|
||||
metrics.total_tokens / metrics.total_successful_requests
|
||||
)}{" "}
|
||||
avg per successful request
|
||||
</Text>
|
||||
<Text>{Math.round(metrics.total_tokens / metrics.total_successful_requests)} avg per successful request</Text>
|
||||
</Card>
|
||||
<Card>
|
||||
<Text>Total Spend</Text>
|
||||
<Title>${formatNumberWithCommas(metrics.total_spend, 2)}</Title>
|
||||
<Text>
|
||||
$
|
||||
{formatNumberWithCommas(
|
||||
metrics.total_spend / metrics.total_successful_requests,
|
||||
3
|
||||
)}{" "}
|
||||
per successful request
|
||||
${formatNumberWithCommas(metrics.total_spend / metrics.total_successful_requests, 3)} per successful request
|
||||
</Text>
|
||||
</Card>
|
||||
</Grid>
|
||||
|
|
@ -63,12 +47,8 @@ const ModelSection = ({
|
|||
{metrics.top_api_keys.map((keyData, index) => (
|
||||
<div key={keyData.api_key} className="flex justify-between items-center p-3 bg-gray-50 rounded-lg">
|
||||
<div>
|
||||
<Text className="font-medium">
|
||||
{keyData.key_alias || `${keyData.api_key.substring(0, 10)}...`}
|
||||
</Text>
|
||||
{keyData.team_id && (
|
||||
<Text className="text-xs text-gray-500">Team: {keyData.team_id}</Text>
|
||||
)}
|
||||
<Text className="font-medium">{keyData.key_alias || `${keyData.api_key.substring(0, 10)}...`}</Text>
|
||||
{keyData.team_id && <Text className="text-xs text-gray-500">Team: {keyData.team_id}</Text>}
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<Text className="font-medium">${formatNumberWithCommas(keyData.spend, 2)}</Text>
|
||||
|
|
@ -88,17 +68,16 @@ const ModelSection = ({
|
|||
<Card>
|
||||
<div className="flex justify-between items-center">
|
||||
<Title>Total Tokens</Title>
|
||||
<CustomLegend categories={["metrics.prompt_tokens", "metrics.completion_tokens", "metrics.total_tokens"]} colors={["blue", "cyan", "indigo"]} />
|
||||
<CustomLegend
|
||||
categories={["metrics.prompt_tokens", "metrics.completion_tokens", "metrics.total_tokens"]}
|
||||
colors={["blue", "cyan", "indigo"]}
|
||||
/>
|
||||
</div>
|
||||
<AreaChart
|
||||
className="mt-4"
|
||||
data={metrics.daily_data}
|
||||
index="date"
|
||||
categories={[
|
||||
"metrics.prompt_tokens",
|
||||
"metrics.completion_tokens",
|
||||
"metrics.total_tokens",
|
||||
]}
|
||||
categories={["metrics.prompt_tokens", "metrics.completion_tokens", "metrics.total_tokens"]}
|
||||
colors={["blue", "cyan", "indigo"]}
|
||||
valueFormatter={valueFormatter}
|
||||
customTooltip={CustomTooltip}
|
||||
|
|
@ -142,10 +121,7 @@ const ModelSection = ({
|
|||
<div className="flex justify-between items-center">
|
||||
<Title>Success vs Failed Requests</Title>
|
||||
<CustomLegend
|
||||
categories={[
|
||||
"metrics.successful_requests",
|
||||
"metrics.failed_requests",
|
||||
]}
|
||||
categories={["metrics.successful_requests", "metrics.failed_requests"]}
|
||||
colors={["green", "red"]}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -153,10 +129,7 @@ const ModelSection = ({
|
|||
className="mt-4"
|
||||
data={metrics.daily_data}
|
||||
index="date"
|
||||
categories={[
|
||||
"metrics.successful_requests",
|
||||
"metrics.failed_requests",
|
||||
]}
|
||||
categories={["metrics.successful_requests", "metrics.failed_requests"]}
|
||||
colors={["green", "red"]}
|
||||
valueFormatter={valueFormatter}
|
||||
stack
|
||||
|
|
@ -169,33 +142,19 @@ const ModelSection = ({
|
|||
<div className="flex justify-between items-center">
|
||||
<Title>Prompt Caching Metrics</Title>
|
||||
<CustomLegend
|
||||
categories={[
|
||||
"metrics.cache_read_input_tokens",
|
||||
"metrics.cache_creation_input_tokens",
|
||||
]}
|
||||
categories={["metrics.cache_read_input_tokens", "metrics.cache_creation_input_tokens"]}
|
||||
colors={["cyan", "purple"]}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-2">
|
||||
<Text>
|
||||
Cache Read:{" "}
|
||||
{metrics.total_cache_read_input_tokens?.toLocaleString() || 0}{" "}
|
||||
tokens
|
||||
</Text>
|
||||
<Text>
|
||||
Cache Creation:{" "}
|
||||
{metrics.total_cache_creation_input_tokens?.toLocaleString() || 0}{" "}
|
||||
tokens
|
||||
</Text>
|
||||
<Text>Cache Read: {metrics.total_cache_read_input_tokens?.toLocaleString() || 0} tokens</Text>
|
||||
<Text>Cache Creation: {metrics.total_cache_creation_input_tokens?.toLocaleString() || 0} tokens</Text>
|
||||
</div>
|
||||
<AreaChart
|
||||
className="mt-4"
|
||||
data={metrics.daily_data}
|
||||
index="date"
|
||||
categories={[
|
||||
"metrics.cache_read_input_tokens",
|
||||
"metrics.cache_creation_input_tokens",
|
||||
]}
|
||||
categories={["metrics.cache_read_input_tokens", "metrics.cache_creation_input_tokens"]}
|
||||
colors={["cyan", "purple"]}
|
||||
valueFormatter={valueFormatter}
|
||||
customTooltip={CustomTooltip}
|
||||
|
|
@ -207,9 +166,7 @@ const ModelSection = ({
|
|||
);
|
||||
};
|
||||
|
||||
export const ActivityMetrics: React.FC<ActivityMetricsProps> = ({
|
||||
modelMetrics,
|
||||
}) => {
|
||||
export const ActivityMetrics: React.FC<ActivityMetricsProps> = ({ modelMetrics }) => {
|
||||
const modelNames = Object.keys(modelMetrics).sort((a, b) => {
|
||||
if (a === "") return 1;
|
||||
if (b === "") return -1;
|
||||
|
|
@ -246,10 +203,8 @@ export const ActivityMetrics: React.FC<ActivityMetricsProps> = ({
|
|||
totalMetrics.total_successful_requests += model.total_successful_requests;
|
||||
totalMetrics.total_tokens += model.total_tokens;
|
||||
totalMetrics.total_spend += model.total_spend;
|
||||
totalMetrics.total_cache_read_input_tokens +=
|
||||
model.total_cache_read_input_tokens || 0;
|
||||
totalMetrics.total_cache_creation_input_tokens +=
|
||||
model.total_cache_creation_input_tokens || 0;
|
||||
totalMetrics.total_cache_read_input_tokens += model.total_cache_read_input_tokens || 0;
|
||||
totalMetrics.total_cache_creation_input_tokens += model.total_cache_creation_input_tokens || 0;
|
||||
|
||||
// Aggregate daily data
|
||||
model.daily_data.forEach((day) => {
|
||||
|
|
@ -266,23 +221,15 @@ export const ActivityMetrics: React.FC<ActivityMetricsProps> = ({
|
|||
cache_creation_input_tokens: 0,
|
||||
};
|
||||
}
|
||||
totalMetrics.daily_data[day.date].prompt_tokens +=
|
||||
day.metrics.prompt_tokens;
|
||||
totalMetrics.daily_data[day.date].completion_tokens +=
|
||||
day.metrics.completion_tokens;
|
||||
totalMetrics.daily_data[day.date].total_tokens +=
|
||||
day.metrics.total_tokens;
|
||||
totalMetrics.daily_data[day.date].api_requests +=
|
||||
day.metrics.api_requests;
|
||||
totalMetrics.daily_data[day.date].prompt_tokens += day.metrics.prompt_tokens;
|
||||
totalMetrics.daily_data[day.date].completion_tokens += day.metrics.completion_tokens;
|
||||
totalMetrics.daily_data[day.date].total_tokens += day.metrics.total_tokens;
|
||||
totalMetrics.daily_data[day.date].api_requests += day.metrics.api_requests;
|
||||
totalMetrics.daily_data[day.date].spend += day.metrics.spend;
|
||||
totalMetrics.daily_data[day.date].successful_requests +=
|
||||
day.metrics.successful_requests;
|
||||
totalMetrics.daily_data[day.date].failed_requests +=
|
||||
day.metrics.failed_requests;
|
||||
totalMetrics.daily_data[day.date].cache_read_input_tokens +=
|
||||
day.metrics.cache_read_input_tokens || 0;
|
||||
totalMetrics.daily_data[day.date].cache_creation_input_tokens +=
|
||||
day.metrics.cache_creation_input_tokens || 0;
|
||||
totalMetrics.daily_data[day.date].successful_requests += day.metrics.successful_requests;
|
||||
totalMetrics.daily_data[day.date].failed_requests += day.metrics.failed_requests;
|
||||
totalMetrics.daily_data[day.date].cache_read_input_tokens += day.metrics.cache_read_input_tokens || 0;
|
||||
totalMetrics.daily_data[day.date].cache_creation_input_tokens += day.metrics.cache_creation_input_tokens || 0;
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -303,9 +250,7 @@ export const ActivityMetrics: React.FC<ActivityMetricsProps> = ({
|
|||
</Card>
|
||||
<Card>
|
||||
<Text>Total Successful Requests</Text>
|
||||
<Title>
|
||||
{totalMetrics.total_successful_requests.toLocaleString()}
|
||||
</Title>
|
||||
<Title>{totalMetrics.total_successful_requests.toLocaleString()}</Title>
|
||||
</Card>
|
||||
<Card>
|
||||
<Text>Total Tokens</Text>
|
||||
|
|
@ -313,9 +258,7 @@ export const ActivityMetrics: React.FC<ActivityMetricsProps> = ({
|
|||
</Card>
|
||||
<Card>
|
||||
<Text>Total Spend</Text>
|
||||
<Title>
|
||||
${formatNumberWithCommas(totalMetrics.total_spend, 2)}
|
||||
</Title>
|
||||
<Title>${formatNumberWithCommas(totalMetrics.total_spend, 2)}</Title>
|
||||
</Card>
|
||||
</Grid>
|
||||
|
||||
|
|
@ -324,11 +267,7 @@ export const ActivityMetrics: React.FC<ActivityMetricsProps> = ({
|
|||
<div className="flex justify-between items-center">
|
||||
<Title>Total Tokens Over Time</Title>
|
||||
<CustomLegend
|
||||
categories={[
|
||||
"metrics.prompt_tokens",
|
||||
"metrics.completion_tokens",
|
||||
"metrics.total_tokens",
|
||||
]}
|
||||
categories={["metrics.prompt_tokens", "metrics.completion_tokens", "metrics.total_tokens"]}
|
||||
colors={["blue", "cyan", "indigo"]}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -336,11 +275,7 @@ export const ActivityMetrics: React.FC<ActivityMetricsProps> = ({
|
|||
className="mt-4"
|
||||
data={sortedDailyData}
|
||||
index="date"
|
||||
categories={[
|
||||
"metrics.prompt_tokens",
|
||||
"metrics.completion_tokens",
|
||||
"metrics.total_tokens",
|
||||
]}
|
||||
categories={["metrics.prompt_tokens", "metrics.completion_tokens", "metrics.total_tokens"]}
|
||||
colors={["blue", "cyan", "indigo"]}
|
||||
valueFormatter={valueFormatter}
|
||||
customTooltip={CustomTooltip}
|
||||
|
|
@ -373,25 +308,13 @@ export const ActivityMetrics: React.FC<ActivityMetricsProps> = ({
|
|||
<div className="flex justify-between items-center w-full">
|
||||
<Title>{modelMetrics[modelName].label || "Unknown Item"}</Title>
|
||||
<div className="flex space-x-4 text-sm text-gray-500">
|
||||
<span>
|
||||
$
|
||||
{formatNumberWithCommas(
|
||||
modelMetrics[modelName].total_spend,
|
||||
2
|
||||
)}
|
||||
</span>
|
||||
<span>
|
||||
{modelMetrics[modelName].total_requests.toLocaleString()}{" "}
|
||||
requests
|
||||
</span>
|
||||
<span>${formatNumberWithCommas(modelMetrics[modelName].total_spend, 2)}</span>
|
||||
<span>{modelMetrics[modelName].total_requests.toLocaleString()} requests</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<ModelSection
|
||||
modelName={modelName || "Unknown Model"}
|
||||
metrics={modelMetrics[modelName]}
|
||||
/>
|
||||
<ModelSection modelName={modelName || "Unknown Model"} metrics={modelMetrics[modelName]} />
|
||||
</Collapse.Panel>
|
||||
))}
|
||||
</Collapse>
|
||||
|
|
@ -400,27 +323,24 @@ export const ActivityMetrics: React.FC<ActivityMetricsProps> = ({
|
|||
};
|
||||
|
||||
// Helper function to format key label
|
||||
const formatKeyLabel = (
|
||||
modelData: KeyMetricWithMetadata,
|
||||
model: string
|
||||
): string => {
|
||||
const formatKeyLabel = (modelData: KeyMetricWithMetadata, model: string): string => {
|
||||
const keyAlias = modelData.metadata.key_alias || `key-hash-${model}`;
|
||||
const teamId = modelData.metadata.team_id;
|
||||
return teamId ? `${keyAlias} (team_id: ${teamId})` : keyAlias;
|
||||
};
|
||||
|
||||
// Process data function
|
||||
export const processActivityData = (dailyActivity: { results: DailyData[] }, key: "models" | "api_keys" | "mcp_servers"): Record<string, ModelActivityData> => {
|
||||
export const processActivityData = (
|
||||
dailyActivity: { results: DailyData[] },
|
||||
key: "models" | "api_keys" | "mcp_servers",
|
||||
): Record<string, ModelActivityData> => {
|
||||
const modelMetrics: Record<string, ModelActivityData> = {};
|
||||
|
||||
dailyActivity.results.forEach((day) => {
|
||||
Object.entries(day.breakdown[key] || {}).forEach(([model, modelData]) => {
|
||||
if (!modelMetrics[model]) {
|
||||
modelMetrics[model] = {
|
||||
label:
|
||||
key === "api_keys"
|
||||
? formatKeyLabel(modelData as KeyMetricWithMetadata, model)
|
||||
: model,
|
||||
label: key === "api_keys" ? formatKeyLabel(modelData as KeyMetricWithMetadata, model) : model,
|
||||
total_requests: 0,
|
||||
total_successful_requests: 0,
|
||||
total_failed_requests: 0,
|
||||
|
|
@ -431,24 +351,19 @@ export const processActivityData = (dailyActivity: { results: DailyData[] }, key
|
|||
total_cache_read_input_tokens: 0,
|
||||
total_cache_creation_input_tokens: 0,
|
||||
top_api_keys: [],
|
||||
daily_data: []
|
||||
daily_data: [],
|
||||
};
|
||||
}
|
||||
// Update totals
|
||||
modelMetrics[model].total_requests += modelData.metrics.api_requests;
|
||||
modelMetrics[model].prompt_tokens += modelData.metrics.prompt_tokens;
|
||||
modelMetrics[model].completion_tokens +=
|
||||
modelData.metrics.completion_tokens;
|
||||
modelMetrics[model].completion_tokens += modelData.metrics.completion_tokens;
|
||||
modelMetrics[model].total_tokens += modelData.metrics.total_tokens;
|
||||
modelMetrics[model].total_spend += modelData.metrics.spend;
|
||||
modelMetrics[model].total_successful_requests +=
|
||||
modelData.metrics.successful_requests;
|
||||
modelMetrics[model].total_failed_requests +=
|
||||
modelData.metrics.failed_requests;
|
||||
modelMetrics[model].total_cache_read_input_tokens +=
|
||||
modelData.metrics.cache_read_input_tokens || 0;
|
||||
modelMetrics[model].total_cache_creation_input_tokens +=
|
||||
modelData.metrics.cache_creation_input_tokens || 0;
|
||||
modelMetrics[model].total_successful_requests += modelData.metrics.successful_requests;
|
||||
modelMetrics[model].total_failed_requests += modelData.metrics.failed_requests;
|
||||
modelMetrics[model].total_cache_read_input_tokens += modelData.metrics.cache_read_input_tokens || 0;
|
||||
modelMetrics[model].total_cache_creation_input_tokens += modelData.metrics.cache_creation_input_tokens || 0;
|
||||
|
||||
// Add daily data
|
||||
modelMetrics[model].daily_data.push({
|
||||
|
|
@ -461,24 +376,22 @@ export const processActivityData = (dailyActivity: { results: DailyData[] }, key
|
|||
spend: modelData.metrics.spend,
|
||||
successful_requests: modelData.metrics.successful_requests,
|
||||
failed_requests: modelData.metrics.failed_requests,
|
||||
cache_read_input_tokens:
|
||||
modelData.metrics.cache_read_input_tokens || 0,
|
||||
cache_creation_input_tokens:
|
||||
modelData.metrics.cache_creation_input_tokens || 0,
|
||||
cache_read_input_tokens: modelData.metrics.cache_read_input_tokens || 0,
|
||||
cache_creation_input_tokens: modelData.metrics.cache_creation_input_tokens || 0,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Process API key breakdowns for each metric (skip if key is 'api_keys' to avoid duplication)
|
||||
if (key !== 'api_keys') {
|
||||
if (key !== "api_keys") {
|
||||
Object.entries(modelMetrics).forEach(([model, _]) => {
|
||||
const apiKeyBreakdown: Record<string, TopApiKeyData> = {};
|
||||
|
||||
|
||||
// Aggregate API key data across all days
|
||||
dailyActivity.results.forEach((day) => {
|
||||
const modelData = day.breakdown[key]?.[model];
|
||||
if (modelData && 'api_key_breakdown' in modelData) {
|
||||
if (modelData && "api_key_breakdown" in modelData) {
|
||||
Object.entries(modelData.api_key_breakdown || {}).forEach(([apiKey, keyData]) => {
|
||||
if (!apiKeyBreakdown[apiKey]) {
|
||||
apiKeyBreakdown[apiKey] = {
|
||||
|
|
@ -490,7 +403,7 @@ export const processActivityData = (dailyActivity: { results: DailyData[] }, key
|
|||
tokens: 0,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
apiKeyBreakdown[apiKey].spend += keyData.metrics.spend;
|
||||
apiKeyBreakdown[apiKey].requests += keyData.metrics.api_requests;
|
||||
apiKeyBreakdown[apiKey].tokens += keyData.metrics.total_tokens;
|
||||
|
|
@ -507,9 +420,7 @@ export const processActivityData = (dailyActivity: { results: DailyData[] }, key
|
|||
|
||||
// Sort daily data
|
||||
Object.values(modelMetrics).forEach((metrics) => {
|
||||
metrics.daily_data.sort(
|
||||
(a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()
|
||||
);
|
||||
metrics.daily_data.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime());
|
||||
});
|
||||
|
||||
return modelMetrics;
|
||||
|
|
|
|||
|
|
@ -2,33 +2,22 @@
|
|||
* Modal to add fallbacks to the proxy router config
|
||||
*/
|
||||
|
||||
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Button, TextInput, Grid, Col } from "@tremor/react";
|
||||
import { Select, SelectItem, MultiSelect, MultiSelectItem, SearchSelect, SearchSelectItem } from "@tremor/react";
|
||||
import { setCallbacksCall } from "./networking";
|
||||
import {
|
||||
Modal,
|
||||
Form,
|
||||
message,
|
||||
} from "antd";
|
||||
import { Modal, Form, message } from "antd";
|
||||
import { fetchAvailableModels, ModelGroup } from "./chat_ui/llm_calls/fetch_models";
|
||||
import NotificationManager from "./molecules/notifications_manager";
|
||||
|
||||
interface AddFallbacksProps {
|
||||
models?: string[];
|
||||
accessToken: string;
|
||||
routerSettings: { [key: string]: any; }
|
||||
routerSettings: { [key: string]: any };
|
||||
setRouterSettings: React.Dispatch<React.SetStateAction<{ [key: string]: any }>>;
|
||||
}
|
||||
|
||||
const AddFallbacks: React.FC<AddFallbacksProps> = ({
|
||||
models,
|
||||
accessToken,
|
||||
routerSettings,
|
||||
setRouterSettings
|
||||
}) => {
|
||||
const AddFallbacks: React.FC<AddFallbacksProps> = ({ models, accessToken, routerSettings, setRouterSettings }) => {
|
||||
const [form] = Form.useForm();
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
const [selectedModel, setSelectedModel] = useState("");
|
||||
|
|
@ -84,15 +73,15 @@ const AddFallbacks: React.FC<AddFallbacksProps> = ({
|
|||
console.log(updatedRouterSettings);
|
||||
|
||||
const payload = {
|
||||
router_settings: updatedRouterSettings
|
||||
router_settings: updatedRouterSettings,
|
||||
};
|
||||
|
||||
try {
|
||||
setCallbacksCall(accessToken, payload);
|
||||
// Update routerSettings state
|
||||
setRouterSettings(updatedRouterSettings);
|
||||
setCallbacksCall(accessToken, payload);
|
||||
// Update routerSettings state
|
||||
setRouterSettings(updatedRouterSettings);
|
||||
} catch (error) {
|
||||
NotificationManager.fromBackend("Failed to update router settings: " + error);
|
||||
NotificationManager.fromBackend("Failed to update router settings: " + error);
|
||||
}
|
||||
|
||||
NotificationManager.success("router settings updated successfully");
|
||||
|
|
@ -103,14 +92,9 @@ const AddFallbacks: React.FC<AddFallbacksProps> = ({
|
|||
setSelectedModel("");
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Button
|
||||
className="mx-auto"
|
||||
onClick={() => setIsModalVisible(true)}
|
||||
icon={() => <span className="mr-1">+</span>}
|
||||
>
|
||||
<Button className="mx-auto" onClick={() => setIsModalVisible(true)} icon={() => <span className="mr-1">+</span>}>
|
||||
Add Fallbacks
|
||||
</Button>
|
||||
<Modal
|
||||
|
|
@ -133,135 +117,127 @@ const AddFallbacks: React.FC<AddFallbacksProps> = ({
|
|||
<div className="mt-6">
|
||||
<div className="mb-6">
|
||||
<p className="text-gray-600">
|
||||
Configure fallback models to improve reliability. When the primary model fails or is unavailable,
|
||||
requests will automatically route to the specified fallback models in order.
|
||||
Configure fallback models to improve reliability. When the primary model fails or is unavailable, requests
|
||||
will automatically route to the specified fallback models in order.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Form
|
||||
form={form}
|
||||
onFinish={updateFallbacks}
|
||||
layout="vertical"
|
||||
className="space-y-6"
|
||||
>
|
||||
<div className="grid grid-cols-1 gap-6">
|
||||
<Form.Item
|
||||
label={
|
||||
<span className="text-sm font-medium text-gray-700">
|
||||
Primary Model <span className="text-red-500">*</span>
|
||||
</span>
|
||||
}
|
||||
name="model_name"
|
||||
rules={[{ required: true, message: 'Please select the primary model that needs fallbacks' }]}
|
||||
className="!mb-0"
|
||||
|
||||
<Form form={form} onFinish={updateFallbacks} layout="vertical" className="space-y-6">
|
||||
<div className="grid grid-cols-1 gap-6">
|
||||
<Form.Item
|
||||
label={
|
||||
<span className="text-sm font-medium text-gray-700">
|
||||
Primary Model <span className="text-red-500">*</span>
|
||||
</span>
|
||||
}
|
||||
name="model_name"
|
||||
rules={[{ required: true, message: "Please select the primary model that needs fallbacks" }]}
|
||||
className="!mb-0"
|
||||
>
|
||||
<SearchSelect
|
||||
placeholder="Select the model that needs fallback protection"
|
||||
value={selectedModel}
|
||||
onValueChange={(value: string) => {
|
||||
setSelectedModel(value);
|
||||
// Remove the selected model from fallbacks if it was selected
|
||||
const updatedFallbacks = selectedFallbacks.filter((model) => model !== value);
|
||||
setSelectedFallbacks(updatedFallbacks);
|
||||
form.setFieldValue("models", updatedFallbacks);
|
||||
form.setFieldValue("model_name", value);
|
||||
}}
|
||||
>
|
||||
<SearchSelect
|
||||
placeholder="Select the model that needs fallback protection"
|
||||
value={selectedModel}
|
||||
onValueChange={(value: string) => {
|
||||
setSelectedModel(value);
|
||||
// Remove the selected model from fallbacks if it was selected
|
||||
const updatedFallbacks = selectedFallbacks.filter(model => model !== value);
|
||||
setSelectedFallbacks(updatedFallbacks);
|
||||
form.setFieldValue('models', updatedFallbacks);
|
||||
form.setFieldValue('model_name', value);
|
||||
}}
|
||||
>
|
||||
{Array.from(new Set(modelInfo.map(option => option.model_group))).map((model: string, index: number) => (
|
||||
<SearchSelectItem
|
||||
key={index}
|
||||
value={model}
|
||||
>
|
||||
{Array.from(new Set(modelInfo.map((option) => option.model_group))).map(
|
||||
(model: string, index: number) => (
|
||||
<SearchSelectItem key={index} value={model}>
|
||||
{model}
|
||||
</SearchSelectItem>
|
||||
))}
|
||||
</SearchSelect>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
This is the primary model that users will request
|
||||
</p>
|
||||
</Form.Item>
|
||||
),
|
||||
)}
|
||||
</SearchSelect>
|
||||
<p className="text-sm text-gray-500 mt-1">This is the primary model that users will request</p>
|
||||
</Form.Item>
|
||||
|
||||
<div className="border-t border-gray-200 my-6"></div>
|
||||
<div className="border-t border-gray-200 my-6"></div>
|
||||
|
||||
<Form.Item
|
||||
label={
|
||||
<span className="text-sm font-medium text-gray-700">
|
||||
Fallback Models (select multiple) <span className="text-red-500">*</span>
|
||||
</span>
|
||||
}
|
||||
name="models"
|
||||
rules={[{ required: true, message: 'Please select at least one fallback model' }]}
|
||||
className="!mb-0"
|
||||
>
|
||||
<div className="space-y-3">
|
||||
{/* Show selected models in order */}
|
||||
{selectedFallbacks.length > 0 && (
|
||||
<div className="border border-gray-200 rounded-lg p-3 bg-gray-50">
|
||||
<p className="text-sm font-medium text-gray-700 mb-2">
|
||||
Fallback Order:
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{selectedFallbacks.map((model, index) => (
|
||||
<div key={model} className="flex items-center bg-blue-100 text-blue-800 px-3 py-1 rounded-full text-sm">
|
||||
<span className="font-medium mr-2">{index + 1}.</span>
|
||||
<span>{model}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const newFallbacks = selectedFallbacks.filter(m => m !== model);
|
||||
setSelectedFallbacks(newFallbacks);
|
||||
form.setFieldValue('models', newFallbacks);
|
||||
}}
|
||||
className="ml-2 text-blue-600 hover:text-blue-800"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Model selector */}
|
||||
<SearchSelect
|
||||
placeholder="Add a fallback model"
|
||||
value=""
|
||||
onValueChange={(value: string) => {
|
||||
if (value && !selectedFallbacks.includes(value)) {
|
||||
const newFallbacks = [...selectedFallbacks, value];
|
||||
setSelectedFallbacks(newFallbacks);
|
||||
form.setFieldValue('models', newFallbacks);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{Array.from(new Set(modelInfo.map(option => option.model_group)))
|
||||
.filter((data: string) => data !== selectedModel && !selectedFallbacks.includes(data))
|
||||
.sort()
|
||||
.map((model: string) => (
|
||||
<SearchSelectItem key={model} value={model}>
|
||||
{model}
|
||||
</SearchSelectItem>
|
||||
<Form.Item
|
||||
label={
|
||||
<span className="text-sm font-medium text-gray-700">
|
||||
Fallback Models (select multiple) <span className="text-red-500">*</span>
|
||||
</span>
|
||||
}
|
||||
name="models"
|
||||
rules={[{ required: true, message: "Please select at least one fallback model" }]}
|
||||
className="!mb-0"
|
||||
>
|
||||
<div className="space-y-3">
|
||||
{/* Show selected models in order */}
|
||||
{selectedFallbacks.length > 0 && (
|
||||
<div className="border border-gray-200 rounded-lg p-3 bg-gray-50">
|
||||
<p className="text-sm font-medium text-gray-700 mb-2">Fallback Order:</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{selectedFallbacks.map((model, index) => (
|
||||
<div
|
||||
key={model}
|
||||
className="flex items-center bg-blue-100 text-blue-800 px-3 py-1 rounded-full text-sm"
|
||||
>
|
||||
<span className="font-medium mr-2">{index + 1}.</span>
|
||||
<span>{model}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const newFallbacks = selectedFallbacks.filter((m) => m !== model);
|
||||
setSelectedFallbacks(newFallbacks);
|
||||
form.setFieldValue("models", newFallbacks);
|
||||
}}
|
||||
className="ml-2 text-blue-600 hover:text-blue-800"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</SearchSelect>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
<strong>Order matters:</strong> Models will be tried in the order shown above (1st, 2nd, 3rd, etc.)
|
||||
</p>
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end space-x-3 pt-6 border-t border-gray-100">
|
||||
<Button variant="secondary" onClick={handleCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="primary" type="submit">
|
||||
Add Fallbacks
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Model selector */}
|
||||
<SearchSelect
|
||||
placeholder="Add a fallback model"
|
||||
value=""
|
||||
onValueChange={(value: string) => {
|
||||
if (value && !selectedFallbacks.includes(value)) {
|
||||
const newFallbacks = [...selectedFallbacks, value];
|
||||
setSelectedFallbacks(newFallbacks);
|
||||
form.setFieldValue("models", newFallbacks);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{Array.from(new Set(modelInfo.map((option) => option.model_group)))
|
||||
.filter((data: string) => data !== selectedModel && !selectedFallbacks.includes(data))
|
||||
.sort()
|
||||
.map((model: string) => (
|
||||
<SearchSelectItem key={model} value={model}>
|
||||
{model}
|
||||
</SearchSelectItem>
|
||||
))}
|
||||
</SearchSelect>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
<strong>Order matters:</strong> Models will be tried in the order shown above (1st, 2nd, 3rd, etc.)
|
||||
</p>
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end space-x-3 pt-6 border-t border-gray-100">
|
||||
<Button variant="secondary" onClick={handleCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="primary" type="submit">
|
||||
Add Fallbacks
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -22,19 +22,12 @@ interface AddAutoRouterTabProps {
|
|||
|
||||
const { Title, Link } = Typography;
|
||||
|
||||
const AddAutoRouterTab: React.FC<AddAutoRouterTabProps> = ({
|
||||
form,
|
||||
handleOk,
|
||||
accessToken,
|
||||
userRole,
|
||||
}) => {
|
||||
const AddAutoRouterTab: React.FC<AddAutoRouterTabProps> = ({ form, handleOk, accessToken, userRole }) => {
|
||||
// State for connection testing
|
||||
const [isResultModalVisible, setIsResultModalVisible] = useState<boolean>(false);
|
||||
const [isTestingConnection, setIsTestingConnection] = useState<boolean>(false);
|
||||
const [connectionTestId, setConnectionTestId] = useState<string>("");
|
||||
|
||||
|
||||
|
||||
const [modelAccessGroups, setModelAccessGroups] = useState<string[]>([]);
|
||||
const [modelInfo, setModelInfo] = useState<ModelGroup[]>([]);
|
||||
const [showCustomDefaultModel, setShowCustomDefaultModel] = useState<boolean>(false);
|
||||
|
|
@ -77,13 +70,13 @@ const AddAutoRouterTab: React.FC<AddAutoRouterTabProps> = ({
|
|||
console.log("Router config:", routerConfig);
|
||||
const currentFormValues = form.getFieldsValue();
|
||||
console.log("Form values:", currentFormValues);
|
||||
|
||||
|
||||
// Check basic required fields first
|
||||
if (!currentFormValues.auto_router_name) {
|
||||
NotificationManager.fromBackend("Please enter an Auto Router Name");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (!currentFormValues.auto_router_default_model) {
|
||||
NotificationManager.fromBackend("Please select a Default Model");
|
||||
return;
|
||||
|
|
@ -91,12 +84,12 @@ const AddAutoRouterTab: React.FC<AddAutoRouterTabProps> = ({
|
|||
|
||||
// Set auto router specific form values that are required by the regular model form
|
||||
form.setFieldsValue({
|
||||
custom_llm_provider: 'auto_router',
|
||||
custom_llm_provider: "auto_router",
|
||||
model: currentFormValues.auto_router_name,
|
||||
// api_key is not needed for auto router, but form expects it
|
||||
api_key: 'not_required_for_auto_router'
|
||||
api_key: "not_required_for_auto_router",
|
||||
});
|
||||
|
||||
|
||||
// Custom validation for router config
|
||||
if (!routerConfig || !routerConfig.routes || routerConfig.routes.length === 0) {
|
||||
NotificationManager.fromBackend("Please configure at least one route for the auto router");
|
||||
|
|
@ -104,12 +97,14 @@ const AddAutoRouterTab: React.FC<AddAutoRouterTabProps> = ({
|
|||
}
|
||||
|
||||
// Check if all routes have required fields
|
||||
const invalidRoutes = routerConfig.routes.filter((route: any) =>
|
||||
!route.name || !route.description || route.utterances.length === 0
|
||||
const invalidRoutes = routerConfig.routes.filter(
|
||||
(route: any) => !route.name || !route.description || route.utterances.length === 0,
|
||||
);
|
||||
|
||||
|
||||
if (invalidRoutes.length > 0) {
|
||||
NotificationManager.fromBackend("Please ensure all routes have a target model, description, and at least one utterance");
|
||||
NotificationManager.fromBackend(
|
||||
"Please ensure all routes have a target model, description, and at least one utterance",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -127,20 +122,20 @@ const AddAutoRouterTab: React.FC<AddAutoRouterTabProps> = ({
|
|||
})
|
||||
.catch((error) => {
|
||||
console.error("Validation failed:", error);
|
||||
|
||||
|
||||
// Extract specific field errors
|
||||
const fieldErrors = error.errorFields || [];
|
||||
if (fieldErrors.length > 0) {
|
||||
const missingFields = fieldErrors.map((field: any) => {
|
||||
const fieldName = field.name[0];
|
||||
const friendlyNames: { [key: string]: string } = {
|
||||
'auto_router_name': 'Auto Router Name',
|
||||
'auto_router_default_model': 'Default Model',
|
||||
'auto_router_embedding_model': 'Embedding Model'
|
||||
auto_router_name: "Auto Router Name",
|
||||
auto_router_default_model: "Default Model",
|
||||
auto_router_embedding_model: "Embedding Model",
|
||||
};
|
||||
return friendlyNames[fieldName] || fieldName;
|
||||
});
|
||||
NotificationManager.fromBackend(`Please fill in the following required fields: ${missingFields.join(', ')}`);
|
||||
NotificationManager.fromBackend(`Please fill in the following required fields: ${missingFields.join(", ")}`);
|
||||
} else {
|
||||
NotificationManager.fromBackend("Please fill in all required fields");
|
||||
}
|
||||
|
|
@ -151,9 +146,10 @@ const AddAutoRouterTab: React.FC<AddAutoRouterTabProps> = ({
|
|||
<>
|
||||
<Title level={2}>Add Auto Router</Title>
|
||||
<Text className="text-gray-600 mb-6">
|
||||
Create an auto router with intelligent routing logic that automatically selects the best model based on user input patterns and semantic matching.
|
||||
Create an auto router with intelligent routing logic that automatically selects the best model based on user
|
||||
input patterns and semantic matching.
|
||||
</Text>
|
||||
|
||||
|
||||
<Card>
|
||||
<Form
|
||||
form={form}
|
||||
|
|
@ -181,7 +177,7 @@ const AddAutoRouterTab: React.FC<AddAutoRouterTabProps> = ({
|
|||
value={routerConfig}
|
||||
onChange={(config) => {
|
||||
setRouterConfig(config);
|
||||
form.setFieldValue('auto_router_config', config);
|
||||
form.setFieldValue("auto_router_config", config);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -198,15 +194,14 @@ const AddAutoRouterTab: React.FC<AddAutoRouterTabProps> = ({
|
|||
<AntdSelect
|
||||
placeholder="Select a default model"
|
||||
onChange={(value) => {
|
||||
setShowCustomDefaultModel(value === 'custom');
|
||||
setShowCustomDefaultModel(value === "custom");
|
||||
}}
|
||||
options={[
|
||||
...Array.from(new Set(modelInfo.map(option => option.model_group)))
|
||||
.map((model_group) => ({
|
||||
value: model_group,
|
||||
label: model_group,
|
||||
})),
|
||||
{ value: 'custom', label: 'Enter custom model name' }
|
||||
...Array.from(new Set(modelInfo.map((option) => option.model_group))).map((model_group) => ({
|
||||
value: model_group,
|
||||
label: model_group,
|
||||
})),
|
||||
{ value: "custom", label: "Enter custom model name" },
|
||||
]}
|
||||
style={{ width: "100%" }}
|
||||
showSearch={true}
|
||||
|
|
@ -222,19 +217,18 @@ const AddAutoRouterTab: React.FC<AddAutoRouterTabProps> = ({
|
|||
labelAlign="left"
|
||||
>
|
||||
<AntdSelect
|
||||
value={form.getFieldValue('auto_router_embedding_model')}
|
||||
value={form.getFieldValue("auto_router_embedding_model")}
|
||||
placeholder="Select an embedding model (optional)"
|
||||
onChange={(value) => {
|
||||
setShowCustomEmbeddingModel(value === 'custom');
|
||||
form.setFieldValue('auto_router_embedding_model', value);
|
||||
setShowCustomEmbeddingModel(value === "custom");
|
||||
form.setFieldValue("auto_router_embedding_model", value);
|
||||
}}
|
||||
options={[
|
||||
...Array.from(new Set(modelInfo.map(option => option.model_group)))
|
||||
.map((model_group) => ({
|
||||
value: model_group,
|
||||
label: model_group,
|
||||
})),
|
||||
{ value: 'custom', label: 'Enter custom model name' }
|
||||
...Array.from(new Set(modelInfo.map((option) => option.model_group))).map((model_group) => ({
|
||||
value: model_group,
|
||||
label: model_group,
|
||||
})),
|
||||
{ value: "custom", label: "Enter custom model name" },
|
||||
]}
|
||||
style={{ width: "100%" }}
|
||||
showSearch={true}
|
||||
|
|
@ -247,8 +241,6 @@ const AddAutoRouterTab: React.FC<AddAutoRouterTabProps> = ({
|
|||
<div className="flex-grow border-t border-gray-200"></div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
{/* Model Access Groups - Admin only */}
|
||||
{isAdmin && (
|
||||
<Form.Item
|
||||
|
|
@ -262,10 +254,10 @@ const AddAutoRouterTab: React.FC<AddAutoRouterTabProps> = ({
|
|||
showSearch
|
||||
placeholder="Select existing groups or type to create new ones"
|
||||
optionFilterProp="children"
|
||||
tokenSeparators={[',']}
|
||||
tokenSeparators={[","]}
|
||||
options={modelAccessGroups.map((group) => ({
|
||||
value: group,
|
||||
label: group
|
||||
label: group,
|
||||
}))}
|
||||
maxTagCount="responsive"
|
||||
allowClear
|
||||
|
|
@ -275,13 +267,13 @@ const AddAutoRouterTab: React.FC<AddAutoRouterTabProps> = ({
|
|||
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<Tooltip title="Get help on our github">
|
||||
<Typography.Link href="https://github.com/BerriAI/litellm/issues">
|
||||
Need Help?
|
||||
</Typography.Link>
|
||||
<Typography.Link href="https://github.com/BerriAI/litellm/issues">Need Help?</Typography.Link>
|
||||
</Tooltip>
|
||||
<div className="space-x-2">
|
||||
<Button onClick={handleTestConnection} loading={isTestingConnection}>Test Connect</Button>
|
||||
<Button
|
||||
<Button onClick={handleTestConnection} loading={isTestingConnection}>
|
||||
Test Connect
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
console.log("Add Auto Router button clicked!");
|
||||
console.log("Current router config:", routerConfig);
|
||||
|
|
@ -295,7 +287,7 @@ const AddAutoRouterTab: React.FC<AddAutoRouterTabProps> = ({
|
|||
</div>
|
||||
</Form>
|
||||
</Card>
|
||||
|
||||
|
||||
{/* Test Connection Results Modal */}
|
||||
<Modal
|
||||
title="Connection Test Results"
|
||||
|
|
@ -305,23 +297,26 @@ const AddAutoRouterTab: React.FC<AddAutoRouterTabProps> = ({
|
|||
setIsTestingConnection(false);
|
||||
}}
|
||||
footer={[
|
||||
<Button key="close" onClick={() => {
|
||||
setIsResultModalVisible(false);
|
||||
setIsTestingConnection(false);
|
||||
}}>
|
||||
<Button
|
||||
key="close"
|
||||
onClick={() => {
|
||||
setIsResultModalVisible(false);
|
||||
setIsTestingConnection(false);
|
||||
}}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</Button>,
|
||||
]}
|
||||
width={700}
|
||||
>
|
||||
{/* Only render the ConnectionErrorDisplay when modal is visible and we have a test ID */}
|
||||
{isResultModalVisible && (
|
||||
<ConnectionErrorDisplay
|
||||
<ConnectionErrorDisplay
|
||||
key={connectionTestId}
|
||||
formValues={form.getFieldsValue()}
|
||||
accessToken={accessToken}
|
||||
testMode="chat"
|
||||
modelName={form.getFieldValue('auto_router_name')}
|
||||
modelName={form.getFieldValue("auto_router_name")}
|
||||
onClose={() => {
|
||||
setIsResultModalVisible(false);
|
||||
setIsTestingConnection(false);
|
||||
|
|
@ -334,4 +329,4 @@ const AddAutoRouterTab: React.FC<AddAutoRouterTabProps> = ({
|
|||
);
|
||||
};
|
||||
|
||||
export default AddAutoRouterTab;
|
||||
export default AddAutoRouterTab;
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ export const TEST_MODES = [
|
|||
{ value: "audio_transcription", label: "Audio Transcription - /audio/transcriptions" },
|
||||
{ value: "image_generation", label: "Image Generation - /images/generations" },
|
||||
{ value: "rerank", label: "Rerank - /rerank" },
|
||||
{ value: "realtime", label: "Realtime - /realtime"},
|
||||
{ value: "batch", label: "Batch - /batch"}
|
||||
{ value: "realtime", label: "Realtime - /realtime" },
|
||||
{ value: "batch", label: "Batch - /batch" },
|
||||
];
|
||||
|
||||
// Define the available auto router routing strategies
|
||||
|
|
@ -18,5 +18,5 @@ export const AUTO_ROUTER_MODES = [
|
|||
{ value: "latency-based", label: "Latency Based - Route to model with best response time" },
|
||||
{ value: "cost-based", label: "Cost Based - Route to most cost-effective model" },
|
||||
{ value: "usage-based", label: "Usage Based - Route based on historical usage patterns" },
|
||||
{ value: "custom", label: "Custom - Use custom routing logic defined in config" }
|
||||
];
|
||||
{ value: "custom", label: "Custom - Use custom routing logic defined in config" },
|
||||
];
|
||||
|
|
|
|||
|
|
@ -66,15 +66,11 @@ const AddModelTab: React.FC<AddModelTabProps> = ({
|
|||
// Using a unique ID to force the ConnectionErrorDisplay to remount and run a fresh test
|
||||
const [connectionTestId, setConnectionTestId] = useState<string>("");
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const fetchGuardrails = async () => {
|
||||
try {
|
||||
const response = await getGuardrailsList(accessToken);
|
||||
const guardrailNames = response.guardrails.map(
|
||||
(g: { guardrail_name: string }) => g.guardrail_name
|
||||
);
|
||||
const guardrailNames = response.guardrails.map((g: { guardrail_name: string }) => g.guardrail_name);
|
||||
setGuardrailsList(guardrailNames);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch guardrails:", error);
|
||||
|
|
@ -83,7 +79,7 @@ const AddModelTab: React.FC<AddModelTabProps> = ({
|
|||
|
||||
fetchGuardrails();
|
||||
}, [accessToken]);
|
||||
|
||||
|
||||
// Test connection when button is clicked
|
||||
const handleTestConnection = async () => {
|
||||
setIsTestingConnection(true);
|
||||
|
|
@ -131,252 +127,243 @@ const AddModelTab: React.FC<AddModelTabProps> = ({
|
|||
<TabPanel>
|
||||
<Title level={2}>Add Model</Title>
|
||||
<Card>
|
||||
<Form
|
||||
form={form}
|
||||
onFinish={(values) => {
|
||||
console.log("🔥 Form onFinish triggered with values:", values);
|
||||
handleOk();
|
||||
}}
|
||||
onFinishFailed={(errorInfo) => {
|
||||
console.log("💥 Form onFinishFailed triggered:", errorInfo);
|
||||
}}
|
||||
labelCol={{ span: 10 }}
|
||||
wrapperCol={{ span: 16 }}
|
||||
labelAlign="left"
|
||||
>
|
||||
<>
|
||||
{/* Provider Selection */}
|
||||
<Form.Item
|
||||
rules={[{ required: true, message: "Required" }]}
|
||||
label="Provider:"
|
||||
name="custom_llm_provider"
|
||||
tooltip="E.g. OpenAI, Azure OpenAI, Anthropic, Bedrock, etc."
|
||||
labelCol={{ span: 10 }}
|
||||
labelAlign="left"
|
||||
>
|
||||
<AntdSelect
|
||||
showSearch={true}
|
||||
value={selectedProvider}
|
||||
onChange={(value) => {
|
||||
setSelectedProvider(value);
|
||||
setProviderModelsFn(value);
|
||||
form.setFieldsValue({
|
||||
model: [],
|
||||
model_name: undefined
|
||||
});
|
||||
<Form
|
||||
form={form}
|
||||
onFinish={(values) => {
|
||||
console.log("🔥 Form onFinish triggered with values:", values);
|
||||
handleOk();
|
||||
}}
|
||||
onFinishFailed={(errorInfo) => {
|
||||
console.log("💥 Form onFinishFailed triggered:", errorInfo);
|
||||
}}
|
||||
labelCol={{ span: 10 }}
|
||||
wrapperCol={{ span: 16 }}
|
||||
labelAlign="left"
|
||||
>
|
||||
{Object.entries(Providers).map(([providerEnum, providerDisplayName]) => (
|
||||
<AntdSelect.Option
|
||||
key={providerEnum}
|
||||
value={providerEnum}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<img
|
||||
src={providerLogoMap[providerDisplayName]}
|
||||
alt={`${providerEnum} logo`}
|
||||
className="w-5 h-5"
|
||||
onError={(e) => {
|
||||
// Create a div with provider initial as fallback
|
||||
const target = e.target as HTMLImageElement;
|
||||
const parent = target.parentElement;
|
||||
if (parent) {
|
||||
const fallbackDiv = document.createElement('div');
|
||||
fallbackDiv.className = 'w-5 h-5 rounded-full bg-gray-200 flex items-center justify-center text-xs';
|
||||
fallbackDiv.textContent = providerDisplayName.charAt(0);
|
||||
parent.replaceChild(fallbackDiv, target);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<span>{providerDisplayName}</span>
|
||||
</div>
|
||||
</AntdSelect.Option>
|
||||
))}
|
||||
</AntdSelect>
|
||||
</Form.Item>
|
||||
<LiteLLMModelNameField
|
||||
selectedProvider={selectedProvider}
|
||||
providerModels={providerModels}
|
||||
getPlaceholder={getPlaceholder}
|
||||
/>
|
||||
|
||||
{/* Conditionally Render "Public Model Name" */}
|
||||
<ConditionalPublicModelName />
|
||||
|
||||
{/* Select Mode */}
|
||||
<Form.Item
|
||||
label="Mode"
|
||||
name="mode"
|
||||
className="mb-1"
|
||||
>
|
||||
<AntdSelect
|
||||
style={{ width: '100%' }}
|
||||
value={testMode}
|
||||
onChange={(value) => setTestMode(value)}
|
||||
options={TEST_MODES}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Row>
|
||||
<Col span={10}></Col>
|
||||
<Col span={10}>
|
||||
<Text className="mb-5 mt-1">
|
||||
<strong>Optional</strong> - LiteLLM endpoint to use when health checking this model <Link href="https://docs.litellm.ai/docs/proxy/health#health" target="_blank">Learn more</Link>
|
||||
</Text>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{/* Credentials */}
|
||||
<div className="mb-4">
|
||||
<Typography.Text className="text-sm text-gray-500 mb-2">
|
||||
Either select existing credentials OR enter new provider credentials below
|
||||
</Typography.Text>
|
||||
</div>
|
||||
|
||||
<Form.Item
|
||||
label="Existing Credentials"
|
||||
name="litellm_credential_name"
|
||||
>
|
||||
<AntdSelect
|
||||
showSearch
|
||||
placeholder="Select or search for existing credentials"
|
||||
optionFilterProp="children"
|
||||
filterOption={(input, option) =>
|
||||
(option?.label ?? '').toLowerCase().includes(input.toLowerCase())
|
||||
}
|
||||
options={[
|
||||
{ value: null, label: 'None' },
|
||||
...credentials.map((credential) => ({
|
||||
value: credential.credential_name,
|
||||
label: credential.credential_name
|
||||
}))
|
||||
]}
|
||||
allowClear
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<div className="flex items-center my-4">
|
||||
<div className="flex-grow border-t border-gray-200"></div>
|
||||
<span className="px-4 text-gray-500 text-sm">OR</span>
|
||||
<div className="flex-grow border-t border-gray-200"></div>
|
||||
</div>
|
||||
|
||||
<Form.Item
|
||||
noStyle
|
||||
shouldUpdate={(prevValues, currentValues) =>
|
||||
prevValues.litellm_credential_name !== currentValues.litellm_credential_name ||
|
||||
prevValues.provider !== currentValues.provider
|
||||
}
|
||||
>
|
||||
{({ getFieldValue }) => {
|
||||
const credentialName = getFieldValue('litellm_credential_name');
|
||||
console.log("🔑 Credential Name Changed:", credentialName);
|
||||
// Only show provider specific fields if no credentials selected
|
||||
if (!credentialName) {
|
||||
return (
|
||||
<ProviderSpecificFields
|
||||
selectedProvider={selectedProvider}
|
||||
uploadProps={uploadProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="text-gray-500 text-sm text-center">
|
||||
Using existing credentials - no additional provider fields needed
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
<div className="flex items-center my-4">
|
||||
<div className="flex-grow border-t border-gray-200"></div>
|
||||
<span className="px-4 text-gray-500 text-sm">Additional Model Info Settings</span>
|
||||
<div className="flex-grow border-t border-gray-200"></div>
|
||||
</div>
|
||||
{/* Team-only Model Switch */}
|
||||
<Form.Item
|
||||
label="Team-BYOK Model"
|
||||
tooltip="Only use this model + credential combination for this team. Useful when teams want to onboard their own OpenAI keys."
|
||||
className="mb-4"
|
||||
>
|
||||
<Tooltip
|
||||
title={!premiumUser ? "This is an enterprise-only feature. Upgrade to premium to restrict model+credential combinations to a specific team." : ""}
|
||||
placement="top"
|
||||
>
|
||||
<Switch
|
||||
checked={isTeamOnly}
|
||||
onChange={(checked) => {
|
||||
setIsTeamOnly(checked);
|
||||
if (!checked) {
|
||||
form.setFieldValue('team_id', undefined);
|
||||
}
|
||||
}}
|
||||
disabled={!premiumUser}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Form.Item>
|
||||
|
||||
{/* Conditional Team Selection */}
|
||||
{isTeamOnly && (
|
||||
<Form.Item
|
||||
label="Select Team"
|
||||
name="team_id"
|
||||
className="mb-4"
|
||||
tooltip="Only keys for this team will be able to call this model."
|
||||
rules={[
|
||||
{
|
||||
required: isTeamOnly && !isAdmin,
|
||||
message: 'Please select a team.'
|
||||
}
|
||||
]}
|
||||
>
|
||||
<TeamDropdown teams={teams} disabled={!premiumUser} />
|
||||
</Form.Item>
|
||||
)}
|
||||
{
|
||||
isAdmin && (
|
||||
<>
|
||||
{/* Provider Selection */}
|
||||
<Form.Item
|
||||
label="Model Access Group"
|
||||
name="model_access_group"
|
||||
className="mb-4"
|
||||
tooltip="Use model access groups to give users access to select models, and add new ones to the group over time."
|
||||
rules={[{ required: true, message: "Required" }]}
|
||||
label="Provider:"
|
||||
name="custom_llm_provider"
|
||||
tooltip="E.g. OpenAI, Azure OpenAI, Anthropic, Bedrock, etc."
|
||||
labelCol={{ span: 10 }}
|
||||
labelAlign="left"
|
||||
>
|
||||
<AntdSelect
|
||||
mode="tags"
|
||||
showSearch={true}
|
||||
value={selectedProvider}
|
||||
onChange={(value) => {
|
||||
setSelectedProvider(value);
|
||||
setProviderModelsFn(value);
|
||||
form.setFieldsValue({
|
||||
model: [],
|
||||
model_name: undefined,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{Object.entries(Providers).map(([providerEnum, providerDisplayName]) => (
|
||||
<AntdSelect.Option key={providerEnum} value={providerEnum}>
|
||||
<div className="flex items-center space-x-2">
|
||||
<img
|
||||
src={providerLogoMap[providerDisplayName]}
|
||||
alt={`${providerEnum} logo`}
|
||||
className="w-5 h-5"
|
||||
onError={(e) => {
|
||||
// Create a div with provider initial as fallback
|
||||
const target = e.target as HTMLImageElement;
|
||||
const parent = target.parentElement;
|
||||
if (parent) {
|
||||
const fallbackDiv = document.createElement("div");
|
||||
fallbackDiv.className =
|
||||
"w-5 h-5 rounded-full bg-gray-200 flex items-center justify-center text-xs";
|
||||
fallbackDiv.textContent = providerDisplayName.charAt(0);
|
||||
parent.replaceChild(fallbackDiv, target);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<span>{providerDisplayName}</span>
|
||||
</div>
|
||||
</AntdSelect.Option>
|
||||
))}
|
||||
</AntdSelect>
|
||||
</Form.Item>
|
||||
<LiteLLMModelNameField
|
||||
selectedProvider={selectedProvider}
|
||||
providerModels={providerModels}
|
||||
getPlaceholder={getPlaceholder}
|
||||
/>
|
||||
|
||||
{/* Conditionally Render "Public Model Name" */}
|
||||
<ConditionalPublicModelName />
|
||||
|
||||
{/* Select Mode */}
|
||||
<Form.Item label="Mode" name="mode" className="mb-1">
|
||||
<AntdSelect
|
||||
style={{ width: "100%" }}
|
||||
value={testMode}
|
||||
onChange={(value) => setTestMode(value)}
|
||||
options={TEST_MODES}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Row>
|
||||
<Col span={10}></Col>
|
||||
<Col span={10}>
|
||||
<Text className="mb-5 mt-1">
|
||||
<strong>Optional</strong> - LiteLLM endpoint to use when health checking this model{" "}
|
||||
<Link href="https://docs.litellm.ai/docs/proxy/health#health" target="_blank">
|
||||
Learn more
|
||||
</Link>
|
||||
</Text>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{/* Credentials */}
|
||||
<div className="mb-4">
|
||||
<Typography.Text className="text-sm text-gray-500 mb-2">
|
||||
Either select existing credentials OR enter new provider credentials below
|
||||
</Typography.Text>
|
||||
</div>
|
||||
|
||||
<Form.Item label="Existing Credentials" name="litellm_credential_name">
|
||||
<AntdSelect
|
||||
showSearch
|
||||
placeholder="Select existing groups or type to create new ones"
|
||||
placeholder="Select or search for existing credentials"
|
||||
optionFilterProp="children"
|
||||
tokenSeparators={[',']}
|
||||
options={modelAccessGroups.map((group) => ({
|
||||
value: group,
|
||||
label: group
|
||||
}))}
|
||||
maxTagCount="responsive"
|
||||
filterOption={(input, option) =>
|
||||
(option?.label ?? "").toLowerCase().includes(input.toLowerCase())
|
||||
}
|
||||
options={[
|
||||
{ value: null, label: "None" },
|
||||
...credentials.map((credential) => ({
|
||||
value: credential.credential_name,
|
||||
label: credential.credential_name,
|
||||
})),
|
||||
]}
|
||||
allowClear
|
||||
/>
|
||||
</Form.Item>
|
||||
</>
|
||||
)
|
||||
}
|
||||
<AdvancedSettings
|
||||
showAdvancedSettings={showAdvancedSettings}
|
||||
setShowAdvancedSettings={setShowAdvancedSettings}
|
||||
teams={teams}
|
||||
guardrailsList={guardrailsList}
|
||||
/>
|
||||
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<Tooltip title="Get help on our github">
|
||||
<Typography.Link href="https://github.com/BerriAI/litellm/issues">
|
||||
Need Help?
|
||||
</Typography.Link>
|
||||
</Tooltip>
|
||||
<div className="space-x-2">
|
||||
<Button onClick={handleTestConnection} loading={isTestingConnection}>Test Connect</Button>
|
||||
<Button htmlType="submit">Add Model</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</Form>
|
||||
<div className="flex items-center my-4">
|
||||
<div className="flex-grow border-t border-gray-200"></div>
|
||||
<span className="px-4 text-gray-500 text-sm">OR</span>
|
||||
<div className="flex-grow border-t border-gray-200"></div>
|
||||
</div>
|
||||
|
||||
<Form.Item
|
||||
noStyle
|
||||
shouldUpdate={(prevValues, currentValues) =>
|
||||
prevValues.litellm_credential_name !== currentValues.litellm_credential_name ||
|
||||
prevValues.provider !== currentValues.provider
|
||||
}
|
||||
>
|
||||
{({ getFieldValue }) => {
|
||||
const credentialName = getFieldValue("litellm_credential_name");
|
||||
console.log("🔑 Credential Name Changed:", credentialName);
|
||||
// Only show provider specific fields if no credentials selected
|
||||
if (!credentialName) {
|
||||
return <ProviderSpecificFields selectedProvider={selectedProvider} uploadProps={uploadProps} />;
|
||||
}
|
||||
return (
|
||||
<div className="text-gray-500 text-sm text-center">
|
||||
Using existing credentials - no additional provider fields needed
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
<div className="flex items-center my-4">
|
||||
<div className="flex-grow border-t border-gray-200"></div>
|
||||
<span className="px-4 text-gray-500 text-sm">Additional Model Info Settings</span>
|
||||
<div className="flex-grow border-t border-gray-200"></div>
|
||||
</div>
|
||||
{/* Team-only Model Switch */}
|
||||
<Form.Item
|
||||
label="Team-BYOK Model"
|
||||
tooltip="Only use this model + credential combination for this team. Useful when teams want to onboard their own OpenAI keys."
|
||||
className="mb-4"
|
||||
>
|
||||
<Tooltip
|
||||
title={
|
||||
!premiumUser
|
||||
? "This is an enterprise-only feature. Upgrade to premium to restrict model+credential combinations to a specific team."
|
||||
: ""
|
||||
}
|
||||
placement="top"
|
||||
>
|
||||
<Switch
|
||||
checked={isTeamOnly}
|
||||
onChange={(checked) => {
|
||||
setIsTeamOnly(checked);
|
||||
if (!checked) {
|
||||
form.setFieldValue("team_id", undefined);
|
||||
}
|
||||
}}
|
||||
disabled={!premiumUser}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Form.Item>
|
||||
|
||||
{/* Conditional Team Selection */}
|
||||
{isTeamOnly && (
|
||||
<Form.Item
|
||||
label="Select Team"
|
||||
name="team_id"
|
||||
className="mb-4"
|
||||
tooltip="Only keys for this team will be able to call this model."
|
||||
rules={[
|
||||
{
|
||||
required: isTeamOnly && !isAdmin,
|
||||
message: "Please select a team.",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<TeamDropdown teams={teams} disabled={!premiumUser} />
|
||||
</Form.Item>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<>
|
||||
<Form.Item
|
||||
label="Model Access Group"
|
||||
name="model_access_group"
|
||||
className="mb-4"
|
||||
tooltip="Use model access groups to give users access to select models, and add new ones to the group over time."
|
||||
>
|
||||
<AntdSelect
|
||||
mode="tags"
|
||||
showSearch
|
||||
placeholder="Select existing groups or type to create new ones"
|
||||
optionFilterProp="children"
|
||||
tokenSeparators={[","]}
|
||||
options={modelAccessGroups.map((group) => ({
|
||||
value: group,
|
||||
label: group,
|
||||
}))}
|
||||
maxTagCount="responsive"
|
||||
allowClear
|
||||
/>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
<AdvancedSettings
|
||||
showAdvancedSettings={showAdvancedSettings}
|
||||
setShowAdvancedSettings={setShowAdvancedSettings}
|
||||
teams={teams}
|
||||
guardrailsList={guardrailsList}
|
||||
/>
|
||||
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<Tooltip title="Get help on our github">
|
||||
<Typography.Link href="https://github.com/BerriAI/litellm/issues">Need Help?</Typography.Link>
|
||||
</Tooltip>
|
||||
<div className="space-x-2">
|
||||
<Button onClick={handleTestConnection} loading={isTestingConnection}>
|
||||
Test Connect
|
||||
</Button>
|
||||
<Button htmlType="submit">Add Model</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</Form>
|
||||
</Card>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
|
|
@ -389,7 +376,7 @@ const AddModelTab: React.FC<AddModelTabProps> = ({
|
|||
</TabPanel>
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
|
||||
|
||||
{/* Test Connection Results Modal */}
|
||||
<Modal
|
||||
title="Connection Test Results"
|
||||
|
|
@ -399,24 +386,27 @@ const AddModelTab: React.FC<AddModelTabProps> = ({
|
|||
setIsTestingConnection(false);
|
||||
}}
|
||||
footer={[
|
||||
<Button key="close" onClick={() => {
|
||||
setIsResultModalVisible(false);
|
||||
setIsTestingConnection(false);
|
||||
}}>
|
||||
<Button
|
||||
key="close"
|
||||
onClick={() => {
|
||||
setIsResultModalVisible(false);
|
||||
setIsTestingConnection(false);
|
||||
}}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</Button>,
|
||||
]}
|
||||
width={700}
|
||||
>
|
||||
{/* Only render the ConnectionErrorDisplay when modal is visible and we have a test ID */}
|
||||
{isResultModalVisible && (
|
||||
<ConnectionErrorDisplay
|
||||
<ConnectionErrorDisplay
|
||||
// The key prop tells React to create a fresh component instance when it changes
|
||||
key={connectionTestId}
|
||||
formValues={form.getFieldsValue()}
|
||||
accessToken={accessToken}
|
||||
testMode={testMode}
|
||||
modelName={form.getFieldValue('model_name') || form.getFieldValue('model')}
|
||||
modelName={form.getFieldValue("model_name") || form.getFieldValue("model")}
|
||||
onClose={() => {
|
||||
setIsResultModalVisible(false);
|
||||
setIsTestingConnection(false);
|
||||
|
|
@ -429,4 +419,4 @@ const AddModelTab: React.FC<AddModelTabProps> = ({
|
|||
);
|
||||
};
|
||||
|
||||
export default AddModelTab;
|
||||
export default AddModelTab;
|
||||
|
|
|
|||
|
|
@ -24,10 +24,8 @@ const AdvancedSettings: React.FC<AdvancedSettingsProps> = ({
|
|||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
const [customPricing, setCustomPricing] = React.useState(false);
|
||||
const [pricingModel, setPricingModel] = React.useState<'per_token' | 'per_second'>('per_token');
|
||||
const [pricingModel, setPricingModel] = React.useState<"per_token" | "per_second">("per_token");
|
||||
const [showCacheControl, setShowCacheControl] = React.useState(false);
|
||||
|
||||
|
||||
|
||||
// Add validation function for numbers
|
||||
const validateNumber = (_: any, value: string) => {
|
||||
|
|
@ -35,7 +33,7 @@ const AdvancedSettings: React.FC<AdvancedSettingsProps> = ({
|
|||
return Promise.resolve();
|
||||
}
|
||||
if (isNaN(Number(value)) || Number(value) < 0) {
|
||||
return Promise.reject('Please enter a valid positive number');
|
||||
return Promise.reject("Please enter a valid positive number");
|
||||
}
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
|
@ -48,7 +46,7 @@ const AdvancedSettings: React.FC<AdvancedSettingsProps> = ({
|
|||
JSON.parse(value);
|
||||
return Promise.resolve();
|
||||
} catch (error) {
|
||||
return Promise.reject('Please enter valid JSON');
|
||||
return Promise.reject("Please enter valid JSON");
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -66,7 +64,7 @@ const AdvancedSettings: React.FC<AdvancedSettingsProps> = ({
|
|||
};
|
||||
|
||||
const handlePassThroughChange = (checked: boolean) => {
|
||||
const currentParams = form.getFieldValue('litellm_extra_params');
|
||||
const currentParams = form.getFieldValue("litellm_extra_params");
|
||||
try {
|
||||
let paramsObj = currentParams ? JSON.parse(currentParams) : {};
|
||||
if (checked) {
|
||||
|
|
@ -76,16 +74,16 @@ const AdvancedSettings: React.FC<AdvancedSettingsProps> = ({
|
|||
}
|
||||
// Only set the field value if there are remaining parameters
|
||||
if (Object.keys(paramsObj).length > 0) {
|
||||
form.setFieldValue('litellm_extra_params', JSON.stringify(paramsObj, null, 2));
|
||||
form.setFieldValue("litellm_extra_params", JSON.stringify(paramsObj, null, 2));
|
||||
} else {
|
||||
form.setFieldValue('litellm_extra_params', '');
|
||||
form.setFieldValue("litellm_extra_params", "");
|
||||
}
|
||||
} catch (error) {
|
||||
// If JSON parsing fails, only create new object if checked is true
|
||||
if (checked) {
|
||||
form.setFieldValue('litellm_extra_params', JSON.stringify({ use_in_pass_through: true }, null, 2));
|
||||
form.setFieldValue("litellm_extra_params", JSON.stringify({ use_in_pass_through: true }, null, 2));
|
||||
} else {
|
||||
form.setFieldValue('litellm_extra_params', '');
|
||||
form.setFieldValue("litellm_extra_params", "");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -93,17 +91,17 @@ const AdvancedSettings: React.FC<AdvancedSettingsProps> = ({
|
|||
const handleCacheControlChange = (checked: boolean) => {
|
||||
setShowCacheControl(checked);
|
||||
if (!checked) {
|
||||
const currentParams = form.getFieldValue('litellm_extra_params');
|
||||
const currentParams = form.getFieldValue("litellm_extra_params");
|
||||
try {
|
||||
let paramsObj = currentParams ? JSON.parse(currentParams) : {};
|
||||
delete paramsObj.cache_control_injection_points;
|
||||
if (Object.keys(paramsObj).length > 0) {
|
||||
form.setFieldValue('litellm_extra_params', JSON.stringify(paramsObj, null, 2));
|
||||
form.setFieldValue("litellm_extra_params", JSON.stringify(paramsObj, null, 2));
|
||||
} else {
|
||||
form.setFieldValue('litellm_extra_params', '');
|
||||
form.setFieldValue("litellm_extra_params", "");
|
||||
}
|
||||
} catch (error) {
|
||||
form.setFieldValue('litellm_extra_params', '');
|
||||
form.setFieldValue("litellm_extra_params", "");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -116,63 +114,52 @@ const AdvancedSettings: React.FC<AdvancedSettingsProps> = ({
|
|||
</AccordionHeader>
|
||||
<AccordionBody>
|
||||
<div className="bg-white rounded-lg">
|
||||
|
||||
<Form.Item
|
||||
label="Custom Pricing"
|
||||
name="custom_pricing"
|
||||
valuePropName="checked"
|
||||
className="mb-4"
|
||||
>
|
||||
<Form.Item label="Custom Pricing" name="custom_pricing" valuePropName="checked" className="mb-4">
|
||||
<Switch onChange={handleCustomPricingChange} className="bg-gray-600" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
<Form.Item
|
||||
label={
|
||||
<span>
|
||||
Guardrails{' '}
|
||||
Guardrails{" "}
|
||||
<Tooltip title="Apply safety guardrails to this key to filter content or enforce policies">
|
||||
<a
|
||||
href="https://docs.litellm.ai/docs/proxy/guardrails/quick_start"
|
||||
target="_blank"
|
||||
<a
|
||||
href="https://docs.litellm.ai/docs/proxy/guardrails/quick_start"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={(e) => e.stopPropagation()} // Prevent accordion from collapsing when clicking link
|
||||
>
|
||||
<InfoCircleOutlined style={{ marginLeft: '4px' }} />
|
||||
</a>
|
||||
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
|
||||
</a>
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
name="guardrails"
|
||||
name="guardrails"
|
||||
className="mt-4"
|
||||
help="Select existing guardrails. Go to 'Guardrails' tab to create new guardrails."
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
style={{ width: '100%' }}
|
||||
style={{ width: "100%" }}
|
||||
placeholder="Select or enter guardrails"
|
||||
options={guardrailsList.map(name => ({ value: name, label: name }))}
|
||||
options={guardrailsList.map((name) => ({ value: name, label: name }))}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
|
||||
{customPricing && (
|
||||
<div className="ml-6 pl-4 border-l-2 border-gray-200">
|
||||
<Form.Item
|
||||
label="Pricing Model"
|
||||
name="pricing_model"
|
||||
className="mb-4"
|
||||
>
|
||||
<Form.Item label="Pricing Model" name="pricing_model" className="mb-4">
|
||||
<Select
|
||||
defaultValue="per_token"
|
||||
onChange={(value: 'per_token' | 'per_second') => setPricingModel(value)}
|
||||
onChange={(value: "per_token" | "per_second") => setPricingModel(value)}
|
||||
options={[
|
||||
{ value: 'per_token', label: 'Per Million Tokens' },
|
||||
{ value: 'per_second', label: 'Per Second' },
|
||||
{ value: "per_token", label: "Per Million Tokens" },
|
||||
{ value: "per_second", label: "Per Second" },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{pricingModel === 'per_token' ? (
|
||||
{pricingModel === "per_token" ? (
|
||||
<>
|
||||
<Form.Item
|
||||
label="Input Cost (per 1M tokens)"
|
||||
|
|
@ -218,10 +205,7 @@ const AdvancedSettings: React.FC<AdvancedSettingsProps> = ({
|
|||
</span>
|
||||
}
|
||||
>
|
||||
<Switch
|
||||
onChange={handlePassThroughChange}
|
||||
className="bg-gray-600"
|
||||
/>
|
||||
<Switch onChange={handlePassThroughChange} className="bg-gray-600" />
|
||||
</Form.Item>
|
||||
|
||||
<CacheControlSettings
|
||||
|
|
@ -234,7 +218,7 @@ const AdvancedSettings: React.FC<AdvancedSettingsProps> = ({
|
|||
name="litellm_extra_params"
|
||||
tooltip="Optional litellm params used for making a litellm.completion() call."
|
||||
className="mb-4 mt-4"
|
||||
rules={[{ validator: validateJSON }]}
|
||||
rules={[{ validator: validateJSON }]}
|
||||
>
|
||||
<TextArea
|
||||
rows={4}
|
||||
|
|
@ -250,10 +234,7 @@ const AdvancedSettings: React.FC<AdvancedSettingsProps> = ({
|
|||
<Col span={10}>
|
||||
<Text className="text-gray-600 text-sm">
|
||||
Pass JSON of litellm supported params{" "}
|
||||
<Link
|
||||
href="https://docs.litellm.ai/docs/completion/input"
|
||||
target="_blank"
|
||||
>
|
||||
<Link href="https://docs.litellm.ai/docs/completion/input" target="_blank">
|
||||
litellm.completion() call
|
||||
</Link>
|
||||
</Text>
|
||||
|
|
@ -264,7 +245,7 @@ const AdvancedSettings: React.FC<AdvancedSettingsProps> = ({
|
|||
name="model_info_params"
|
||||
tooltip="Optional model info params. Returned when calling `/model/info` endpoint."
|
||||
className="mb-0"
|
||||
rules={[{ validator: validateJSON }]}
|
||||
rules={[{ validator: validateJSON }]}
|
||||
>
|
||||
<TextArea
|
||||
rows={4}
|
||||
|
|
@ -280,4 +261,4 @@ const AdvancedSettings: React.FC<AdvancedSettingsProps> = ({
|
|||
);
|
||||
};
|
||||
|
||||
export default AdvancedSettings;
|
||||
export default AdvancedSettings;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React from "react";
|
||||
import { Form, Switch, Select, Input, Typography } from "antd";
|
||||
import { PlusOutlined, MinusCircleOutlined } from '@ant-design/icons';
|
||||
import { PlusOutlined, MinusCircleOutlined } from "@ant-design/icons";
|
||||
import NumericalInput from "../shared/numerical_input";
|
||||
|
||||
const { Text } = Typography;
|
||||
|
|
@ -23,7 +23,7 @@ const CacheControlSettings: React.FC<CacheControlSettingsProps> = ({
|
|||
onCacheControlChange,
|
||||
}) => {
|
||||
const updateCacheControlPoints = (injectionPoints: CacheControlInjectionPoint[]) => {
|
||||
const currentParams = form.getFieldValue('litellm_extra_params');
|
||||
const currentParams = form.getFieldValue("litellm_extra_params");
|
||||
try {
|
||||
let paramsObj = currentParams ? JSON.parse(currentParams) : {};
|
||||
if (injectionPoints.length > 0) {
|
||||
|
|
@ -32,12 +32,12 @@ const CacheControlSettings: React.FC<CacheControlSettingsProps> = ({
|
|||
delete paramsObj.cache_control_injection_points;
|
||||
}
|
||||
if (Object.keys(paramsObj).length > 0) {
|
||||
form.setFieldValue('litellm_extra_params', JSON.stringify(paramsObj, null, 2));
|
||||
form.setFieldValue("litellm_extra_params", JSON.stringify(paramsObj, null, 2));
|
||||
} else {
|
||||
form.setFieldValue('litellm_extra_params', '');
|
||||
form.setFieldValue("litellm_extra_params", "");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error updating cache control points:', error);
|
||||
console.error("Error updating cache control points:", error);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -56,14 +56,11 @@ const CacheControlSettings: React.FC<CacheControlSettingsProps> = ({
|
|||
{showCacheControl && (
|
||||
<div className="ml-6 pl-4 border-l-2 border-gray-200">
|
||||
<Text className="text-sm text-gray-500 block mb-4">
|
||||
Providers like Anthropic, Bedrock API require users to specify where to inject cache control checkpoints,
|
||||
Providers like Anthropic, Bedrock API require users to specify where to inject cache control checkpoints,
|
||||
litellm can automatically add them for you as a cost saving feature.
|
||||
</Text>
|
||||
|
||||
<Form.List
|
||||
name="cache_control_injection_points"
|
||||
initialValue={[{ location: "message" }]}
|
||||
>
|
||||
|
||||
<Form.List name="cache_control_injection_points" initialValue={[{ location: "message" }]}>
|
||||
{(fields, { add, remove }) => (
|
||||
<>
|
||||
{fields.map((field, index) => (
|
||||
|
|
@ -71,43 +68,43 @@ const CacheControlSettings: React.FC<CacheControlSettingsProps> = ({
|
|||
<Form.Item
|
||||
{...field}
|
||||
label="Type"
|
||||
name={[field.name, 'location']}
|
||||
name={[field.name, "location"]}
|
||||
initialValue="message"
|
||||
className="mb-0"
|
||||
style={{ width: '180px' }}
|
||||
style={{ width: "180px" }}
|
||||
>
|
||||
<Select disabled options={[{ value: 'message', label: 'Message' }]} />
|
||||
<Select disabled options={[{ value: "message", label: "Message" }]} />
|
||||
</Form.Item>
|
||||
|
||||
|
||||
<Form.Item
|
||||
{...field}
|
||||
label="Role"
|
||||
name={[field.name, 'role']}
|
||||
name={[field.name, "role"]}
|
||||
className="mb-0"
|
||||
style={{ width: '180px' }}
|
||||
style={{ width: "180px" }}
|
||||
tooltip="LiteLLM will mark all messages of this role as cacheable"
|
||||
>
|
||||
<Select
|
||||
placeholder="Select a role"
|
||||
allowClear
|
||||
options={[
|
||||
{ value: 'user', label: 'User' },
|
||||
{ value: 'system', label: 'System' },
|
||||
{ value: 'assistant', label: 'Assistant' },
|
||||
{ value: "user", label: "User" },
|
||||
{ value: "system", label: "System" },
|
||||
{ value: "assistant", label: "Assistant" },
|
||||
]}
|
||||
onChange={() => {
|
||||
const values = form.getFieldValue('cache_control_points');
|
||||
const values = form.getFieldValue("cache_control_points");
|
||||
updateCacheControlPoints(values);
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
|
||||
<Form.Item
|
||||
{...field}
|
||||
label="Index"
|
||||
name={[field.name, 'index']}
|
||||
name={[field.name, "index"]}
|
||||
className="mb-0"
|
||||
style={{ width: '180px' }}
|
||||
style={{ width: "180px" }}
|
||||
tooltip="(Optional) If set litellm will mark the message at this index as cacheable"
|
||||
>
|
||||
<NumericalInput
|
||||
|
|
@ -116,19 +113,19 @@ const CacheControlSettings: React.FC<CacheControlSettingsProps> = ({
|
|||
step={1}
|
||||
min={0}
|
||||
onChange={() => {
|
||||
const values = form.getFieldValue('cache_control_points');
|
||||
const values = form.getFieldValue("cache_control_points");
|
||||
updateCacheControlPoints(values);
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
|
||||
{fields.length > 1 && (
|
||||
<MinusCircleOutlined
|
||||
<MinusCircleOutlined
|
||||
className="text-red-500 cursor-pointer text-lg ml-12"
|
||||
onClick={() => {
|
||||
remove(field.name);
|
||||
setTimeout(() => {
|
||||
const values = form.getFieldValue('cache_control_points');
|
||||
const values = form.getFieldValue("cache_control_points");
|
||||
updateCacheControlPoints(values);
|
||||
}, 0);
|
||||
}}
|
||||
|
|
@ -136,7 +133,7 @@ const CacheControlSettings: React.FC<CacheControlSettingsProps> = ({
|
|||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
|
||||
<Form.Item>
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -156,4 +153,4 @@ const CacheControlSettings: React.FC<CacheControlSettingsProps> = ({
|
|||
);
|
||||
};
|
||||
|
||||
export default CacheControlSettings;
|
||||
export default CacheControlSettings;
|
||||
|
|
|
|||
|
|
@ -6,152 +6,147 @@ import { Providers } from "../provider_info_helpers";
|
|||
|
||||
const ConditionalPublicModelName: React.FC = () => {
|
||||
const form = Form.useFormInstance();
|
||||
const [tableKey, setTableKey] = useState(0);// Add a key to force table re-render
|
||||
const [tableKey, setTableKey] = useState(0); // Add a key to force table re-render
|
||||
|
||||
// Watch the 'model' field for changes and ensure it's always an array
|
||||
const modelValue = Form.useWatch('model', form) || [];
|
||||
const modelValue = Form.useWatch("model", form) || [];
|
||||
const selectedModels = Array.isArray(modelValue) ? modelValue : [modelValue];
|
||||
const customModelName = Form.useWatch('custom_model_name', form);
|
||||
const showPublicModelName = !selectedModels.includes('all-wildcard');
|
||||
const selectedProvider = Form.useWatch('custom_llm_provider', form);
|
||||
const customModelName = Form.useWatch("custom_model_name", form);
|
||||
const showPublicModelName = !selectedModels.includes("all-wildcard");
|
||||
const selectedProvider = Form.useWatch("custom_llm_provider", form);
|
||||
// Force table to re-render when custom model name changes
|
||||
useEffect(() => {
|
||||
if (customModelName && selectedModels.includes('custom')) {
|
||||
const currentMappings = form.getFieldValue('model_mappings') || [];
|
||||
if (customModelName && selectedModels.includes("custom")) {
|
||||
const currentMappings = form.getFieldValue("model_mappings") || [];
|
||||
const updatedMappings = currentMappings.map((mapping: any) => {
|
||||
if (mapping.public_name === 'custom' || mapping.litellm_model === 'custom') {
|
||||
if (mapping.public_name === "custom" || mapping.litellm_model === "custom") {
|
||||
if (selectedProvider === Providers.Azure) {
|
||||
return {
|
||||
public_name: customModelName,
|
||||
litellm_model: `azure/${customModelName}`
|
||||
litellm_model: `azure/${customModelName}`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
public_name: customModelName,
|
||||
litellm_model: customModelName
|
||||
litellm_model: customModelName,
|
||||
};
|
||||
}
|
||||
return mapping;
|
||||
});
|
||||
form.setFieldValue('model_mappings', updatedMappings);
|
||||
setTableKey(prev => prev + 1); // Force table re-render
|
||||
form.setFieldValue("model_mappings", updatedMappings);
|
||||
setTableKey((prev) => prev + 1); // Force table re-render
|
||||
}
|
||||
}, [customModelName, selectedModels, selectedProvider, form]);
|
||||
|
||||
// Initial setup of model mappings when models are selected
|
||||
useEffect(() => {
|
||||
if (selectedModels.length > 0 && !selectedModels.includes('all-wildcard')) {
|
||||
if (selectedModels.length > 0 && !selectedModels.includes("all-wildcard")) {
|
||||
// Check if we already have mappings that match the selected models
|
||||
const currentMappings = form.getFieldValue('model_mappings') || [];
|
||||
|
||||
const currentMappings = form.getFieldValue("model_mappings") || [];
|
||||
|
||||
// Only update if the mappings don't exist or don't match the selected models
|
||||
const shouldUpdateMappings = currentMappings.length !== selectedModels.length ||
|
||||
!selectedModels.every(model =>
|
||||
const shouldUpdateMappings =
|
||||
currentMappings.length !== selectedModels.length ||
|
||||
!selectedModels.every((model) =>
|
||||
currentMappings.some((mapping: { public_name: string; litellm_model: string }) => {
|
||||
if (model === 'custom') {
|
||||
return mapping.litellm_model === 'custom' || mapping.litellm_model === customModelName;
|
||||
if (model === "custom") {
|
||||
return mapping.litellm_model === "custom" || mapping.litellm_model === customModelName;
|
||||
}
|
||||
if (selectedProvider === Providers.Azure) {
|
||||
return mapping.litellm_model === `azure/${model}`;
|
||||
}
|
||||
return mapping.litellm_model === model;
|
||||
}));
|
||||
|
||||
}),
|
||||
);
|
||||
|
||||
if (shouldUpdateMappings) {
|
||||
const mappings = selectedModels.map((model: string) => {
|
||||
if (model === 'custom' && customModelName) {
|
||||
if (model === "custom" && customModelName) {
|
||||
if (selectedProvider === Providers.Azure) {
|
||||
return {
|
||||
public_name: customModelName,
|
||||
litellm_model: `azure/${customModelName}`
|
||||
litellm_model: `azure/${customModelName}`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
public_name: customModelName,
|
||||
litellm_model: customModelName
|
||||
litellm_model: customModelName,
|
||||
};
|
||||
}
|
||||
if (selectedProvider === Providers.Azure) {
|
||||
return {
|
||||
public_name: model,
|
||||
litellm_model: `azure/${model}`
|
||||
litellm_model: `azure/${model}`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
public_name: model,
|
||||
litellm_model: model
|
||||
litellm_model: model,
|
||||
};
|
||||
});
|
||||
|
||||
form.setFieldValue('model_mappings', mappings);
|
||||
setTableKey(prev => prev + 1); // Force table re-render
|
||||
|
||||
form.setFieldValue("model_mappings", mappings);
|
||||
setTableKey((prev) => prev + 1); // Force table re-render
|
||||
}
|
||||
}
|
||||
}, [selectedModels, customModelName, selectedProvider,form]);
|
||||
}, [selectedModels, customModelName, selectedProvider, form]);
|
||||
|
||||
if (!showPublicModelName) return null;
|
||||
|
||||
const publicNameTooltipContent = (
|
||||
<>
|
||||
<div className="mb-2 font-normal">The name you specify in your API calls to LiteLLM Proxy</div>
|
||||
<div className="mb-2 font-normal">
|
||||
The name you specify in your API calls to LiteLLM Proxy
|
||||
</div>
|
||||
<div className="mb-2 font-normal">
|
||||
<strong>Example:</strong> If you name your public model <code className="bg-gray-700 px-1 py-0.5 rounded text-xs">example-name</code>
|
||||
, and choose <code className="bg-gray-700 px-1 py-0.5 rounded text-xs">openai/qwen-plus-latest</code> as the LiteLLM model
|
||||
<strong>Example:</strong> If you name your public model{" "}
|
||||
<code className="bg-gray-700 px-1 py-0.5 rounded text-xs">example-name</code>, and choose{" "}
|
||||
<code className="bg-gray-700 px-1 py-0.5 rounded text-xs">openai/qwen-plus-latest</code> as the LiteLLM model
|
||||
</div>
|
||||
<div className="mb-2 font-normal">
|
||||
<strong>Usage:</strong> You make an API call to the LiteLLM proxy with <code className="bg-gray-700 px-1 py-0.5 rounded text-xs">model = "example-name"</code>
|
||||
<strong>Usage:</strong> You make an API call to the LiteLLM proxy with{" "}
|
||||
<code className="bg-gray-700 px-1 py-0.5 rounded text-xs">model = "example-name"</code>
|
||||
</div>
|
||||
<div className="font-normal">
|
||||
<strong>Result:</strong> LiteLLM sends <code className="bg-gray-700 px-1 py-0.5 rounded text-xs">qwen-plus-latest</code> to the provider
|
||||
<strong>Result:</strong> LiteLLM sends{" "}
|
||||
<code className="bg-gray-700 px-1 py-0.5 rounded text-xs">qwen-plus-latest</code> to the provider
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
const liteLLMModelTooltipContent = (
|
||||
<div>The model name LiteLLM will send to the LLM API</div>
|
||||
);
|
||||
const liteLLMModelTooltipContent = <div>The model name LiteLLM will send to the LLM API</div>;
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: (
|
||||
<span className="flex items-center">
|
||||
Public Model Name
|
||||
<Tooltip
|
||||
content={publicNameTooltipContent}
|
||||
width="500px"
|
||||
/>
|
||||
<Tooltip content={publicNameTooltipContent} width="500px" />
|
||||
</span>
|
||||
),
|
||||
dataIndex: 'public_name',
|
||||
key: 'public_name',
|
||||
dataIndex: "public_name",
|
||||
key: "public_name",
|
||||
render: (text: string, record: any, index: number) => {
|
||||
return (
|
||||
<TextInput
|
||||
value={text}
|
||||
onChange={(e) => {
|
||||
const newMappings = [...form.getFieldValue('model_mappings')];
|
||||
const newMappings = [...form.getFieldValue("model_mappings")];
|
||||
newMappings[index].public_name = e.target.value;
|
||||
form.setFieldValue('model_mappings', newMappings);
|
||||
form.setFieldValue("model_mappings", newMappings);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<span className="flex items-center">
|
||||
LiteLLM Model Name
|
||||
<Tooltip
|
||||
content={liteLLMModelTooltipContent}
|
||||
width="360px"
|
||||
/>
|
||||
<Tooltip content={liteLLMModelTooltipContent} width="360px" />
|
||||
</span>
|
||||
),
|
||||
dataIndex: 'litellm_model',
|
||||
key: 'litellm_model',
|
||||
}
|
||||
dataIndex: "litellm_model",
|
||||
key: "litellm_model",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
|
|
@ -168,23 +163,23 @@ const ConditionalPublicModelName: React.FC = () => {
|
|||
required: true,
|
||||
validator: async (_, value) => {
|
||||
if (!value || value.length === 0) {
|
||||
throw new Error('At least one model mapping is required');
|
||||
throw new Error("At least one model mapping is required");
|
||||
}
|
||||
// Check if all mappings have valid public names
|
||||
const invalidMappings = value.filter((mapping: any) =>
|
||||
!mapping.public_name || mapping.public_name.trim() === ''
|
||||
const invalidMappings = value.filter(
|
||||
(mapping: any) => !mapping.public_name || mapping.public_name.trim() === "",
|
||||
);
|
||||
if (invalidMappings.length > 0) {
|
||||
throw new Error('All model mappings must have valid public names');
|
||||
throw new Error("All model mappings must have valid public names");
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Table
|
||||
<Table
|
||||
key={tableKey} // Add key to force re-render
|
||||
dataSource={form.getFieldValue('model_mappings')}
|
||||
columns={columns}
|
||||
dataSource={form.getFieldValue("model_mappings")}
|
||||
columns={columns}
|
||||
pagination={false}
|
||||
size="small"
|
||||
/>
|
||||
|
|
@ -193,4 +188,4 @@ const ConditionalPublicModelName: React.FC = () => {
|
|||
);
|
||||
};
|
||||
|
||||
export default ConditionalPublicModelName;
|
||||
export default ConditionalPublicModelName;
|
||||
|
|
|
|||
|
|
@ -2,12 +2,7 @@ import { message } from "antd";
|
|||
import { modelCreateCall, Model } from "../networking";
|
||||
import NotificationManager from "../molecules/notifications_manager";
|
||||
|
||||
export const handleAddAutoRouterSubmit = async (
|
||||
values: any,
|
||||
accessToken: string,
|
||||
form: any,
|
||||
callback?: () => void,
|
||||
) => {
|
||||
export const handleAddAutoRouterSubmit = async (values: any, accessToken: string, form: any, callback?: () => void) => {
|
||||
try {
|
||||
console.log("=== AUTO ROUTER SUBMIT HANDLER CALLED ===");
|
||||
console.log("handling auto router submit for formValues:", values);
|
||||
|
|
@ -27,7 +22,7 @@ export const handleAddAutoRouterSubmit = async (
|
|||
};
|
||||
|
||||
// Add optional embedding model if provided
|
||||
if (values.auto_router_embedding_model && values.auto_router_embedding_model !== 'custom') {
|
||||
if (values.auto_router_embedding_model && values.auto_router_embedding_model !== "custom") {
|
||||
autoRouterConfig.litellm_params.auto_router_embedding_model = values.auto_router_embedding_model;
|
||||
} else if (values.custom_embedding_model) {
|
||||
autoRouterConfig.litellm_params.auto_router_embedding_model = values.custom_embedding_model;
|
||||
|
|
@ -47,15 +42,17 @@ export const handleAddAutoRouterSubmit = async (
|
|||
console.log("Auto router config (stringified):", autoRouterConfig.litellm_params.auto_router_config);
|
||||
|
||||
// Create the auto router using the same model creation endpoint
|
||||
console.log("Calling modelCreateCall with:", { accessToken: accessToken ? "Present" : "Missing", config: autoRouterConfig });
|
||||
console.log("Calling modelCreateCall with:", {
|
||||
accessToken: accessToken ? "Present" : "Missing",
|
||||
config: autoRouterConfig,
|
||||
});
|
||||
const response: any = await modelCreateCall(accessToken, autoRouterConfig as Model);
|
||||
console.log(`response for auto router create call:`, response);
|
||||
|
||||
|
||||
// Reset the form
|
||||
form.resetFields();
|
||||
|
||||
} catch (error) {
|
||||
console.error("Failed to add auto router:", error);
|
||||
NotificationManager.fromBackend("Failed to add auto router: " + error);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,191 +1,168 @@
|
|||
import { message } from "antd";
|
||||
import { provider_map, Providers } from "../provider_info_helpers";
|
||||
import { modelCreateCall, Model } from "../networking";
|
||||
import React, { useState } from 'react';
|
||||
import ConnectionErrorDisplay from './model_connection_test';
|
||||
import React, { useState } from "react";
|
||||
import ConnectionErrorDisplay from "./model_connection_test";
|
||||
import NotificationManager from "../molecules/notifications_manager";
|
||||
|
||||
export const prepareModelAddRequest = async (
|
||||
formValues: Record<string, any>,
|
||||
accessToken: string,
|
||||
form: any,
|
||||
) => {
|
||||
try {
|
||||
console.log("handling submit for formValues:", formValues);
|
||||
export const prepareModelAddRequest = async (formValues: Record<string, any>, accessToken: string, form: any) => {
|
||||
try {
|
||||
console.log("handling submit for formValues:", formValues);
|
||||
|
||||
|
||||
// Get model mappings and safely remove from formValues
|
||||
const modelMappings = formValues["model_mappings"] || [];
|
||||
if ("model_mappings" in formValues) {
|
||||
delete formValues["model_mappings"];
|
||||
}
|
||||
|
||||
|
||||
// Handle wildcard case
|
||||
if (formValues["model"] && formValues["model"].includes("all-wildcard")) {
|
||||
const customProvider: Providers = formValues["custom_llm_provider"];
|
||||
const litellm_custom_provider = provider_map[customProvider as keyof typeof Providers];
|
||||
const wildcardModel = litellm_custom_provider + "/*";
|
||||
formValues["model_name"] = wildcardModel;
|
||||
modelMappings.push({
|
||||
public_name: wildcardModel,
|
||||
litellm_model: wildcardModel,
|
||||
});
|
||||
formValues["model"] = wildcardModel;
|
||||
}
|
||||
|
||||
// Create a deployment for each mapping
|
||||
const deployments = [];
|
||||
for (const mapping of modelMappings) {
|
||||
const litellmParamsObj: Record<string, any> = {};
|
||||
const modelInfoObj: Record<string, any> = {};
|
||||
|
||||
// Set the model name and litellm model from the mapping
|
||||
const modelName = mapping.public_name;
|
||||
litellmParamsObj["model"] = mapping.litellm_model;
|
||||
|
||||
// Handle pricing conversion before processing other fields
|
||||
if (formValues.input_cost_per_token) {
|
||||
formValues.input_cost_per_token = Number(formValues.input_cost_per_token) / 1000000;
|
||||
}
|
||||
if (formValues.output_cost_per_token) {
|
||||
formValues.output_cost_per_token = Number(formValues.output_cost_per_token) / 1000000;
|
||||
}
|
||||
// Keep input_cost_per_second as is, no conversion needed
|
||||
|
||||
// Iterate through the key-value pairs in formValues
|
||||
litellmParamsObj["model"] = mapping.litellm_model;
|
||||
console.log("formValues add deployment:", formValues);
|
||||
for (const [key, value] of Object.entries(formValues)) {
|
||||
if (value === "") {
|
||||
continue;
|
||||
}
|
||||
// Skip the custom_pricing and pricing_model fields as they're only used for UI control
|
||||
if (key === 'custom_pricing' || key === 'pricing_model' || key === 'cache_control') {
|
||||
continue;
|
||||
}
|
||||
if (key == "model_name") {
|
||||
litellmParamsObj["model"] = value;
|
||||
} else if (key == "custom_llm_provider") {
|
||||
console.log("custom_llm_provider:", value);
|
||||
const mappingResult = provider_map[value]; // Get the corresponding value from the mapping
|
||||
litellmParamsObj["custom_llm_provider"] = mappingResult;
|
||||
console.log("custom_llm_provider mappingResult:", mappingResult);
|
||||
} else if (key == "model") {
|
||||
continue;
|
||||
}
|
||||
// Check if key is "base_model"
|
||||
else if (key === "base_model") {
|
||||
// Add key-value pair to model_info dictionary
|
||||
modelInfoObj[key] = value;
|
||||
}
|
||||
else if (key === "team_id") {
|
||||
modelInfoObj["team_id"] = value;
|
||||
}
|
||||
else if (key === "model_access_group") {
|
||||
modelInfoObj["access_groups"] = value;
|
||||
}
|
||||
else if (key == "mode") {
|
||||
console.log("placing mode in modelInfo")
|
||||
modelInfoObj["mode"] = value;
|
||||
|
||||
// remove "mode" from litellmParams
|
||||
delete litellmParamsObj["mode"];
|
||||
}
|
||||
else if (key === "custom_model_name") {
|
||||
litellmParamsObj["model"] = value;
|
||||
} else if (key == "litellm_extra_params") {
|
||||
console.log("litellm_extra_params:", value);
|
||||
let litellmExtraParams = {};
|
||||
if (value && value != undefined) {
|
||||
try {
|
||||
litellmExtraParams = JSON.parse(value);
|
||||
} catch (error) {
|
||||
NotificationManager.fromBackend(
|
||||
"Failed to parse LiteLLM Extra Params: " + error
|
||||
);
|
||||
throw new Error("Failed to parse litellm_extra_params: " + error);
|
||||
}
|
||||
for (const [key, value] of Object.entries(litellmExtraParams)) {
|
||||
litellmParamsObj[key] = value;
|
||||
}
|
||||
}
|
||||
} else if (key == "model_info_params") {
|
||||
console.log("model_info_params:", value);
|
||||
let modelInfoParams = {};
|
||||
if (value && value != undefined) {
|
||||
try {
|
||||
modelInfoParams = JSON.parse(value);
|
||||
} catch (error) {
|
||||
NotificationManager.fromBackend(
|
||||
"Failed to parse LiteLLM Extra Params: " + error
|
||||
);
|
||||
throw new Error("Failed to parse litellm_extra_params: " + error);
|
||||
}
|
||||
for (const [key, value] of Object.entries(modelInfoParams)) {
|
||||
modelInfoObj[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle the pricing fields
|
||||
else if (key === "input_cost_per_token" ||
|
||||
key === "output_cost_per_token" ||
|
||||
key === "input_cost_per_second") {
|
||||
if (value) {
|
||||
litellmParamsObj[key] = Number(value);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if key is any of the specified API related keys
|
||||
else {
|
||||
// Add key-value pair to litellm_params dictionary
|
||||
litellmParamsObj[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
deployments.push({ litellmParamsObj, modelInfoObj, modelName });
|
||||
}
|
||||
|
||||
return deployments;
|
||||
} catch (error) {
|
||||
NotificationManager.fromBackend("Failed to create model: " + error);
|
||||
// Get model mappings and safely remove from formValues
|
||||
const modelMappings = formValues["model_mappings"] || [];
|
||||
if ("model_mappings" in formValues) {
|
||||
delete formValues["model_mappings"];
|
||||
}
|
||||
};
|
||||
|
||||
export const handleAddModelSubmit = async (
|
||||
values: any,
|
||||
accessToken: string,
|
||||
form: any,
|
||||
callback?: () => void,
|
||||
) => {
|
||||
try {
|
||||
const deployments = await prepareModelAddRequest(values, accessToken, form);
|
||||
|
||||
if (!deployments || deployments.length === 0) {
|
||||
return; // Exit if preparation failed or no deployments
|
||||
}
|
||||
|
||||
// Create each deployment
|
||||
for (const deployment of deployments) {
|
||||
const { litellmParamsObj, modelInfoObj, modelName } = deployment;
|
||||
|
||||
const new_model: Model = {
|
||||
model_name: modelName,
|
||||
litellm_params: litellmParamsObj,
|
||||
model_info: modelInfoObj,
|
||||
};
|
||||
|
||||
const response: any = await modelCreateCall(accessToken, new_model);
|
||||
console.log(`response for model create call: ${response["data"]}`);
|
||||
}
|
||||
|
||||
callback && callback();
|
||||
form.resetFields();
|
||||
} catch (error) {
|
||||
NotificationManager.fromBackend("Failed to add model: " + error);
|
||||
// Handle wildcard case
|
||||
if (formValues["model"] && formValues["model"].includes("all-wildcard")) {
|
||||
const customProvider: Providers = formValues["custom_llm_provider"];
|
||||
const litellm_custom_provider = provider_map[customProvider as keyof typeof Providers];
|
||||
const wildcardModel = litellm_custom_provider + "/*";
|
||||
formValues["model_name"] = wildcardModel;
|
||||
modelMappings.push({
|
||||
public_name: wildcardModel,
|
||||
litellm_model: wildcardModel,
|
||||
});
|
||||
formValues["model"] = wildcardModel;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Create a deployment for each mapping
|
||||
const deployments = [];
|
||||
for (const mapping of modelMappings) {
|
||||
const litellmParamsObj: Record<string, any> = {};
|
||||
const modelInfoObj: Record<string, any> = {};
|
||||
|
||||
// Set the model name and litellm model from the mapping
|
||||
const modelName = mapping.public_name;
|
||||
litellmParamsObj["model"] = mapping.litellm_model;
|
||||
|
||||
// Handle pricing conversion before processing other fields
|
||||
if (formValues.input_cost_per_token) {
|
||||
formValues.input_cost_per_token = Number(formValues.input_cost_per_token) / 1000000;
|
||||
}
|
||||
if (formValues.output_cost_per_token) {
|
||||
formValues.output_cost_per_token = Number(formValues.output_cost_per_token) / 1000000;
|
||||
}
|
||||
// Keep input_cost_per_second as is, no conversion needed
|
||||
|
||||
// Iterate through the key-value pairs in formValues
|
||||
litellmParamsObj["model"] = mapping.litellm_model;
|
||||
console.log("formValues add deployment:", formValues);
|
||||
for (const [key, value] of Object.entries(formValues)) {
|
||||
if (value === "") {
|
||||
continue;
|
||||
}
|
||||
// Skip the custom_pricing and pricing_model fields as they're only used for UI control
|
||||
if (key === "custom_pricing" || key === "pricing_model" || key === "cache_control") {
|
||||
continue;
|
||||
}
|
||||
if (key == "model_name") {
|
||||
litellmParamsObj["model"] = value;
|
||||
} else if (key == "custom_llm_provider") {
|
||||
console.log("custom_llm_provider:", value);
|
||||
const mappingResult = provider_map[value]; // Get the corresponding value from the mapping
|
||||
litellmParamsObj["custom_llm_provider"] = mappingResult;
|
||||
console.log("custom_llm_provider mappingResult:", mappingResult);
|
||||
} else if (key == "model") {
|
||||
continue;
|
||||
}
|
||||
// Check if key is "base_model"
|
||||
else if (key === "base_model") {
|
||||
// Add key-value pair to model_info dictionary
|
||||
modelInfoObj[key] = value;
|
||||
} else if (key === "team_id") {
|
||||
modelInfoObj["team_id"] = value;
|
||||
} else if (key === "model_access_group") {
|
||||
modelInfoObj["access_groups"] = value;
|
||||
} else if (key == "mode") {
|
||||
console.log("placing mode in modelInfo");
|
||||
modelInfoObj["mode"] = value;
|
||||
|
||||
// remove "mode" from litellmParams
|
||||
delete litellmParamsObj["mode"];
|
||||
} else if (key === "custom_model_name") {
|
||||
litellmParamsObj["model"] = value;
|
||||
} else if (key == "litellm_extra_params") {
|
||||
console.log("litellm_extra_params:", value);
|
||||
let litellmExtraParams = {};
|
||||
if (value && value != undefined) {
|
||||
try {
|
||||
litellmExtraParams = JSON.parse(value);
|
||||
} catch (error) {
|
||||
NotificationManager.fromBackend("Failed to parse LiteLLM Extra Params: " + error);
|
||||
throw new Error("Failed to parse litellm_extra_params: " + error);
|
||||
}
|
||||
for (const [key, value] of Object.entries(litellmExtraParams)) {
|
||||
litellmParamsObj[key] = value;
|
||||
}
|
||||
}
|
||||
} else if (key == "model_info_params") {
|
||||
console.log("model_info_params:", value);
|
||||
let modelInfoParams = {};
|
||||
if (value && value != undefined) {
|
||||
try {
|
||||
modelInfoParams = JSON.parse(value);
|
||||
} catch (error) {
|
||||
NotificationManager.fromBackend("Failed to parse LiteLLM Extra Params: " + error);
|
||||
throw new Error("Failed to parse litellm_extra_params: " + error);
|
||||
}
|
||||
for (const [key, value] of Object.entries(modelInfoParams)) {
|
||||
modelInfoObj[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle the pricing fields
|
||||
else if (key === "input_cost_per_token" || key === "output_cost_per_token" || key === "input_cost_per_second") {
|
||||
if (value) {
|
||||
litellmParamsObj[key] = Number(value);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if key is any of the specified API related keys
|
||||
else {
|
||||
// Add key-value pair to litellm_params dictionary
|
||||
litellmParamsObj[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
deployments.push({ litellmParamsObj, modelInfoObj, modelName });
|
||||
}
|
||||
|
||||
return deployments;
|
||||
} catch (error) {
|
||||
NotificationManager.fromBackend("Failed to create model: " + error);
|
||||
}
|
||||
};
|
||||
|
||||
export const handleAddModelSubmit = async (values: any, accessToken: string, form: any, callback?: () => void) => {
|
||||
try {
|
||||
const deployments = await prepareModelAddRequest(values, accessToken, form);
|
||||
|
||||
if (!deployments || deployments.length === 0) {
|
||||
return; // Exit if preparation failed or no deployments
|
||||
}
|
||||
|
||||
// Create each deployment
|
||||
for (const deployment of deployments) {
|
||||
const { litellmParamsObj, modelInfoObj, modelName } = deployment;
|
||||
|
||||
const new_model: Model = {
|
||||
model_name: modelName,
|
||||
litellm_params: litellmParamsObj,
|
||||
model_info: modelInfoObj,
|
||||
};
|
||||
|
||||
const response: any = await modelCreateCall(accessToken, new_model);
|
||||
console.log(`response for model create call: ${response["data"]}`);
|
||||
}
|
||||
|
||||
callback && callback();
|
||||
form.resetFields();
|
||||
} catch (error) {
|
||||
NotificationManager.fromBackend("Failed to add model: " + error);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ interface LiteLLMModelNameFieldProps {
|
|||
|
||||
const LiteLLMModelNameField: React.FC<LiteLLMModelNameFieldProps> = ({
|
||||
selectedProvider,
|
||||
providerModels,
|
||||
providerModels,
|
||||
getPlaceholder,
|
||||
}) => {
|
||||
const form = Form.useFormInstance();
|
||||
|
|
@ -20,37 +20,35 @@ const LiteLLMModelNameField: React.FC<LiteLLMModelNameFieldProps> = ({
|
|||
const handleModelChange = (value: string | string[]) => {
|
||||
// Ensure value is always treated as an array
|
||||
const values = Array.isArray(value) ? value : [value];
|
||||
|
||||
|
||||
// If "all-wildcard" is selected, clear the model_name field
|
||||
if (values.includes("all-wildcard")) {
|
||||
form.setFieldsValue({ model_name: undefined, model_mappings: [] });
|
||||
} else {
|
||||
// Get current model value to check if we need to update
|
||||
const currentModel = form.getFieldValue('model');
|
||||
const currentModel = form.getFieldValue("model");
|
||||
|
||||
// Only update if the value has actually changed
|
||||
if (JSON.stringify(currentModel) !== JSON.stringify(values)) {
|
||||
|
||||
// Create mappings first
|
||||
const mappings = values.map(model => {
|
||||
const mappings = values.map((model) => {
|
||||
if (selectedProvider === Providers.Azure) {
|
||||
return {
|
||||
public_name: model,
|
||||
litellm_model: `azure/${model}`
|
||||
litellm_model: `azure/${model}`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
public_name: model,
|
||||
litellm_model: model
|
||||
litellm_model: model,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
// Update both fields in one call to reduce re-renders
|
||||
form.setFieldsValue({
|
||||
form.setFieldsValue({
|
||||
model: values,
|
||||
model_mappings: mappings
|
||||
model_mappings: mappings,
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -59,15 +57,19 @@ const LiteLLMModelNameField: React.FC<LiteLLMModelNameFieldProps> = ({
|
|||
const deploymentName = e.target.value;
|
||||
|
||||
// Create mapping with Azure-specific format
|
||||
const mappings = deploymentName ? [{
|
||||
public_name: deploymentName,
|
||||
litellm_model: `azure/${deploymentName}`
|
||||
}] : [];
|
||||
|
||||
const mappings = deploymentName
|
||||
? [
|
||||
{
|
||||
public_name: deploymentName,
|
||||
litellm_model: `azure/${deploymentName}`,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
// Update both fields
|
||||
form.setFieldsValue({
|
||||
form.setFieldsValue({
|
||||
model: deploymentName,
|
||||
model_mappings: mappings
|
||||
model_mappings: mappings,
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -76,23 +78,23 @@ const LiteLLMModelNameField: React.FC<LiteLLMModelNameFieldProps> = ({
|
|||
const customName = e.target.value;
|
||||
|
||||
// Immediately update the model mappings
|
||||
const currentMappings = form.getFieldValue('model_mappings') || [];
|
||||
const currentMappings = form.getFieldValue("model_mappings") || [];
|
||||
const updatedMappings = currentMappings.map((mapping: any) => {
|
||||
if (mapping.public_name === 'custom' || mapping.litellm_model === 'custom') {
|
||||
if (mapping.public_name === "custom" || mapping.litellm_model === "custom") {
|
||||
if (selectedProvider === Providers.Azure) {
|
||||
return {
|
||||
public_name: customName,
|
||||
litellm_model: `azure/${customName}`
|
||||
litellm_model: `azure/${customName}`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
public_name: customName,
|
||||
litellm_model: customName
|
||||
litellm_model: customName,
|
||||
};
|
||||
}
|
||||
return mapping;
|
||||
});
|
||||
|
||||
|
||||
form.setFieldsValue({ model_mappings: updatedMappings });
|
||||
};
|
||||
|
||||
|
|
@ -105,15 +107,20 @@ const LiteLLMModelNameField: React.FC<LiteLLMModelNameFieldProps> = ({
|
|||
>
|
||||
<Form.Item
|
||||
name="model"
|
||||
rules={[{ required: true, message: `Please enter ${selectedProvider === Providers.Azure ? 'a deployment name' : 'at least one model'}.` }]}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: `Please enter ${selectedProvider === Providers.Azure ? "a deployment name" : "at least one model"}.`,
|
||||
},
|
||||
]}
|
||||
noStyle
|
||||
>
|
||||
{(selectedProvider === Providers.Azure) ||
|
||||
(selectedProvider === Providers.OpenAI_Compatible) ||
|
||||
(selectedProvider === Providers.Ollama) ? (
|
||||
{selectedProvider === Providers.Azure ||
|
||||
selectedProvider === Providers.OpenAI_Compatible ||
|
||||
selectedProvider === Providers.Ollama ? (
|
||||
<>
|
||||
<TextInput
|
||||
placeholder={getPlaceholder(selectedProvider)}
|
||||
<TextInput
|
||||
placeholder={getPlaceholder(selectedProvider)}
|
||||
onChange={selectedProvider === Providers.Azure ? handleAzureDeploymentNameChange : undefined}
|
||||
/>
|
||||
</>
|
||||
|
|
@ -125,24 +132,22 @@ const LiteLLMModelNameField: React.FC<LiteLLMModelNameFieldProps> = ({
|
|||
placeholder="Select models"
|
||||
onChange={handleModelChange}
|
||||
optionFilterProp="children"
|
||||
filterOption={(input, option) =>
|
||||
(option?.label ?? '').toLowerCase().includes(input.toLowerCase())
|
||||
}
|
||||
filterOption={(input, option) => (option?.label ?? "").toLowerCase().includes(input.toLowerCase())}
|
||||
options={[
|
||||
{
|
||||
label: 'Custom Model Name (Enter below)',
|
||||
value: 'custom'
|
||||
label: "Custom Model Name (Enter below)",
|
||||
value: "custom",
|
||||
},
|
||||
{
|
||||
label: `All ${selectedProvider} Models (Wildcard)`,
|
||||
value: 'all-wildcard'
|
||||
value: "all-wildcard",
|
||||
},
|
||||
...providerModels.map(model => ({
|
||||
...providerModels.map((model) => ({
|
||||
label: model,
|
||||
value: model
|
||||
}))
|
||||
value: model,
|
||||
})),
|
||||
]}
|
||||
style={{ width: '100%' }}
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
) : (
|
||||
<TextInput placeholder={getPlaceholder(selectedProvider)} />
|
||||
|
|
@ -150,26 +155,25 @@ const LiteLLMModelNameField: React.FC<LiteLLMModelNameFieldProps> = ({
|
|||
</Form.Item>
|
||||
|
||||
{/* Custom Model Name field */}
|
||||
<Form.Item
|
||||
noStyle
|
||||
shouldUpdate={(prevValues, currentValues) =>
|
||||
prevValues.model !== currentValues.model
|
||||
}
|
||||
>
|
||||
<Form.Item noStyle shouldUpdate={(prevValues, currentValues) => prevValues.model !== currentValues.model}>
|
||||
{({ getFieldValue }) => {
|
||||
const selectedModels = getFieldValue('model') || [];
|
||||
const selectedModels = getFieldValue("model") || [];
|
||||
const modelArray = Array.isArray(selectedModels) ? selectedModels : [selectedModels];
|
||||
return modelArray.includes('custom') && (
|
||||
<Form.Item
|
||||
name="custom_model_name"
|
||||
rules={[{ required: true, message: "Please enter a custom model name." }]}
|
||||
className="mt-2"
|
||||
>
|
||||
<TextInput
|
||||
placeholder={selectedProvider === Providers.Azure ? "Enter Azure deployment name" : "Enter custom model name"}
|
||||
onChange={handleCustomModelNameChange}
|
||||
/>
|
||||
</Form.Item>
|
||||
return (
|
||||
modelArray.includes("custom") && (
|
||||
<Form.Item
|
||||
name="custom_model_name"
|
||||
rules={[{ required: true, message: "Please enter a custom model name." }]}
|
||||
className="mt-2"
|
||||
>
|
||||
<TextInput
|
||||
placeholder={
|
||||
selectedProvider === Providers.Azure ? "Enter Azure deployment name" : "Enter custom model name"
|
||||
}
|
||||
onChange={handleCustomModelNameChange}
|
||||
/>
|
||||
</Form.Item>
|
||||
)
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
|
|
@ -178,10 +182,9 @@ const LiteLLMModelNameField: React.FC<LiteLLMModelNameFieldProps> = ({
|
|||
<Col span={10}></Col>
|
||||
<Col span={14}>
|
||||
<Text className="mb-3 mt-1">
|
||||
{selectedProvider === Providers.Azure
|
||||
{selectedProvider === Providers.Azure
|
||||
? "Your deployment name will be saved as the public model name, and LiteLLM will use 'azure/deployment-name' internally"
|
||||
: "The model name LiteLLM will send to the LLM API"
|
||||
}
|
||||
: "The model name LiteLLM will send to the LLM API"}
|
||||
</Text>
|
||||
</Col>
|
||||
</Row>
|
||||
|
|
@ -189,4 +192,4 @@ const LiteLLMModelNameField: React.FC<LiteLLMModelNameFieldProps> = ({
|
|||
);
|
||||
};
|
||||
|
||||
export default LiteLLMModelNameField;
|
||||
export default LiteLLMModelNameField;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import React from 'react';
|
||||
import { Typography, Space, Button, Divider, message } from 'antd';
|
||||
import { WarningOutlined, InfoCircleOutlined, CopyOutlined } from '@ant-design/icons';
|
||||
import React from "react";
|
||||
import { Typography, Space, Button, Divider, message } from "antd";
|
||||
import { WarningOutlined, InfoCircleOutlined, CopyOutlined } from "@ant-design/icons";
|
||||
import { testConnectionRequest } from "../networking";
|
||||
import { prepareModelAddRequest } from "./handle_add_model_submit";
|
||||
import NotificationsManager from '../molecules/notifications_manager';
|
||||
import NotificationsManager from "../molecules/notifications_manager";
|
||||
const { Text } = Typography;
|
||||
|
||||
interface ModelConnectionTestProps {
|
||||
|
|
@ -15,13 +15,13 @@ interface ModelConnectionTestProps {
|
|||
onTestComplete?: () => void;
|
||||
}
|
||||
|
||||
const ModelConnectionTest: React.FC<ModelConnectionTestProps> = ({
|
||||
formValues,
|
||||
accessToken,
|
||||
testMode,
|
||||
modelName = "this model",
|
||||
const ModelConnectionTest: React.FC<ModelConnectionTestProps> = ({
|
||||
formValues,
|
||||
accessToken,
|
||||
testMode,
|
||||
modelName = "this model",
|
||||
onClose,
|
||||
onTestComplete
|
||||
onTestComplete,
|
||||
}) => {
|
||||
const [error, setError] = React.useState<Error | string | null>(null);
|
||||
const [rawRequest, setRawRequest] = React.useState<any>(null);
|
||||
|
|
@ -37,14 +37,14 @@ const ModelConnectionTest: React.FC<ModelConnectionTestProps> = ({
|
|||
setRawRequest(null);
|
||||
setRawResponse(null);
|
||||
setIsSuccess(false);
|
||||
|
||||
|
||||
// Add a small delay to ensure form values are fully populated
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
try {
|
||||
console.log("Testing connection with form values:", formValues);
|
||||
const result = await prepareModelAddRequest(formValues, accessToken, null);
|
||||
|
||||
|
||||
if (!result) {
|
||||
console.log("No result from prepareModelAddRequest");
|
||||
setError("Failed to prepare model data. Please check your form inputs.");
|
||||
|
|
@ -85,153 +85,192 @@ const ModelConnectionTest: React.FC<ModelConnectionTestProps> = ({
|
|||
const timer = setTimeout(() => {
|
||||
testModelConnection();
|
||||
}, 200);
|
||||
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, []); // Empty dependency array means this runs once on mount
|
||||
|
||||
const getCleanErrorMessage = (errorMsg: string) => {
|
||||
if (!errorMsg) return "Unknown error";
|
||||
|
||||
const mainError = errorMsg.split('stack trace:')[0].trim();
|
||||
|
||||
const cleanedError = mainError.replace(/^litellm\.(.*?)Error: /, '');
|
||||
|
||||
|
||||
const mainError = errorMsg.split("stack trace:")[0].trim();
|
||||
|
||||
const cleanedError = mainError.replace(/^litellm\.(.*?)Error: /, "");
|
||||
|
||||
return cleanedError;
|
||||
};
|
||||
|
||||
const errorMessage = typeof error === 'string'
|
||||
? getCleanErrorMessage(error)
|
||||
: error?.message ? getCleanErrorMessage(error.message) : "Unknown error";
|
||||
const errorMessage =
|
||||
typeof error === "string"
|
||||
? getCleanErrorMessage(error)
|
||||
: error?.message
|
||||
? getCleanErrorMessage(error.message)
|
||||
: "Unknown error";
|
||||
|
||||
const formatCurlCommand = (apiBase: string, requestBody: Record<string, any>, requestHeaders: Record<string, string>) => {
|
||||
const formatCurlCommand = (
|
||||
apiBase: string,
|
||||
requestBody: Record<string, any>,
|
||||
requestHeaders: Record<string, string>,
|
||||
) => {
|
||||
const formattedBody = JSON.stringify(requestBody, null, 2)
|
||||
.split('\n')
|
||||
.map(line => ` ${line}`)
|
||||
.join('\n');
|
||||
.split("\n")
|
||||
.map((line) => ` ${line}`)
|
||||
.join("\n");
|
||||
|
||||
const headerString = Object.entries(requestHeaders)
|
||||
.map(([key, value]) => `-H '${key}: ${value}'`)
|
||||
.join(' \\\n ');
|
||||
.join(" \\\n ");
|
||||
|
||||
return `curl -X POST \\
|
||||
${apiBase} \\
|
||||
${headerString ? `${headerString} \\\n ` : ''}-H 'Content-Type: application/json' \\
|
||||
${headerString ? `${headerString} \\\n ` : ""}-H 'Content-Type: application/json' \\
|
||||
-d '{
|
||||
${formattedBody}
|
||||
}'`;
|
||||
};
|
||||
|
||||
const curlCommand = rawResponse ? formatCurlCommand(
|
||||
rawResponse.raw_request_api_base,
|
||||
rawResponse.raw_request_body,
|
||||
rawResponse.raw_request_headers || {}
|
||||
) : '';
|
||||
const curlCommand = rawResponse
|
||||
? formatCurlCommand(
|
||||
rawResponse.raw_request_api_base,
|
||||
rawResponse.raw_request_body,
|
||||
rawResponse.raw_request_headers || {},
|
||||
)
|
||||
: "";
|
||||
|
||||
return (
|
||||
<div style={{ padding: '24px', borderRadius: '8px', backgroundColor: '#fff' }}>
|
||||
<div style={{ padding: "24px", borderRadius: "8px", backgroundColor: "#fff" }}>
|
||||
{isLoading ? (
|
||||
<div style={{ textAlign: 'center', padding: '32px 20px' }}>
|
||||
<div className="loading-spinner" style={{ marginBottom: '16px' }}>
|
||||
<div style={{ textAlign: "center", padding: "32px 20px" }}>
|
||||
<div className="loading-spinner" style={{ marginBottom: "16px" }}>
|
||||
{/* Simple CSS spinner */}
|
||||
<div style={{
|
||||
border: '3px solid #f3f3f3',
|
||||
borderTop: '3px solid #1890ff',
|
||||
borderRadius: '50%',
|
||||
width: '30px',
|
||||
height: '30px',
|
||||
animation: 'spin 1s linear infinite',
|
||||
margin: '0 auto'
|
||||
}} />
|
||||
<div
|
||||
style={{
|
||||
border: "3px solid #f3f3f3",
|
||||
borderTop: "3px solid #1890ff",
|
||||
borderRadius: "50%",
|
||||
width: "30px",
|
||||
height: "30px",
|
||||
animation: "spin 1s linear infinite",
|
||||
margin: "0 auto",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Text style={{ fontSize: '16px' }}>Testing connection to {modelName}...</Text>
|
||||
<Text style={{ fontSize: "16px" }}>Testing connection to {modelName}...</Text>
|
||||
<style jsx>{`
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
) : isSuccess ? (
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '32px 20px' }}>
|
||||
<div style={{ color: '#52c41a', fontSize: '24px', display: 'flex', alignItems: 'center' }}>
|
||||
<svg viewBox="64 64 896 896" focusable="false" data-icon="check-circle" width="1em" height="1em" fill="currentColor" aria-hidden="true">
|
||||
<div style={{ display: "flex", alignItems: "center", justifyContent: "center", padding: "32px 20px" }}>
|
||||
<div style={{ color: "#52c41a", fontSize: "24px", display: "flex", alignItems: "center" }}>
|
||||
<svg
|
||||
viewBox="64 64 896 896"
|
||||
focusable="false"
|
||||
data-icon="check-circle"
|
||||
width="1em"
|
||||
height="1em"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm193.5 301.7l-210.6 292a31.8 31.8 0 01-51.7 0L318.5 484.9c-3.8-5.3 0-12.7 6.5-12.7h46.9c10.2 0 19.9 4.9 25.9 13.3l71.2 98.8 157.2-218c6-8.3 15.6-13.3 25.9-13.3H699c6.5 0 10.3 7.4 6.5 12.7z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<Text type="success" style={{ fontSize: '18px', fontWeight: 500, marginLeft: '10px' }}>
|
||||
<Text type="success" style={{ fontSize: "18px", fontWeight: 500, marginLeft: "10px" }}>
|
||||
Connection to {modelName} successful!
|
||||
</Text>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', marginBottom: '20px' }}>
|
||||
<WarningOutlined style={{ color: '#ff4d4f', fontSize: '24px', marginRight: '12px' }} />
|
||||
<Text type="danger" style={{ fontSize: '18px', fontWeight: 500 }}>Connection to {modelName} failed</Text>
|
||||
<div style={{ display: "flex", alignItems: "center", marginBottom: "20px" }}>
|
||||
<WarningOutlined style={{ color: "#ff4d4f", fontSize: "24px", marginRight: "12px" }} />
|
||||
<Text type="danger" style={{ fontSize: "18px", fontWeight: 500 }}>
|
||||
Connection to {modelName} failed
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
backgroundColor: '#fff2f0',
|
||||
border: '1px solid #ffccc7',
|
||||
borderRadius: '8px',
|
||||
padding: '16px',
|
||||
marginBottom: '20px',
|
||||
boxShadow: '0 1px 2px rgba(0, 0, 0, 0.03)'
|
||||
}}>
|
||||
<Text strong style={{ display: 'block', marginBottom: '8px' }}>Error: </Text>
|
||||
<Text type="danger" style={{ fontSize: '14px', lineHeight: '1.5' }}>{errorMessage}</Text>
|
||||
|
||||
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: "#fff2f0",
|
||||
border: "1px solid #ffccc7",
|
||||
borderRadius: "8px",
|
||||
padding: "16px",
|
||||
marginBottom: "20px",
|
||||
boxShadow: "0 1px 2px rgba(0, 0, 0, 0.03)",
|
||||
}}
|
||||
>
|
||||
<Text strong style={{ display: "block", marginBottom: "8px" }}>
|
||||
Error:{" "}
|
||||
</Text>
|
||||
<Text type="danger" style={{ fontSize: "14px", lineHeight: "1.5" }}>
|
||||
{errorMessage}
|
||||
</Text>
|
||||
|
||||
{error && (
|
||||
<div style={{ marginTop: '12px' }}>
|
||||
<Button
|
||||
type="link"
|
||||
<div style={{ marginTop: "12px" }}>
|
||||
<Button
|
||||
type="link"
|
||||
onClick={() => setShowDetails(!showDetails)}
|
||||
style={{ paddingLeft: 0, height: 'auto' }}
|
||||
style={{ paddingLeft: 0, height: "auto" }}
|
||||
>
|
||||
{showDetails ? 'Hide Details' : 'Show Details'}
|
||||
{showDetails ? "Hide Details" : "Show Details"}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
{showDetails && (
|
||||
<div style={{ marginBottom: '20px' }}>
|
||||
<Text strong style={{ display: 'block', marginBottom: '8px', fontSize: '15px' }}>Troubleshooting Details</Text>
|
||||
<pre style={{
|
||||
backgroundColor: '#f5f5f5',
|
||||
padding: '16px',
|
||||
borderRadius: '8px',
|
||||
fontSize: '13px',
|
||||
maxHeight: '200px',
|
||||
overflow: 'auto',
|
||||
border: '1px solid #e8e8e8',
|
||||
lineHeight: '1.5'
|
||||
}}>
|
||||
{typeof error === 'string' ? error : JSON.stringify(error, null, 2)}
|
||||
<div style={{ marginBottom: "20px" }}>
|
||||
<Text strong style={{ display: "block", marginBottom: "8px", fontSize: "15px" }}>
|
||||
Troubleshooting Details
|
||||
</Text>
|
||||
<pre
|
||||
style={{
|
||||
backgroundColor: "#f5f5f5",
|
||||
padding: "16px",
|
||||
borderRadius: "8px",
|
||||
fontSize: "13px",
|
||||
maxHeight: "200px",
|
||||
overflow: "auto",
|
||||
border: "1px solid #e8e8e8",
|
||||
lineHeight: "1.5",
|
||||
}}
|
||||
>
|
||||
{typeof error === "string" ? error : JSON.stringify(error, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
<div>
|
||||
<Text strong style={{ display: 'block', marginBottom: '8px', fontSize: '15px' }}>API Request</Text>
|
||||
<pre style={{
|
||||
backgroundColor: '#f5f5f5',
|
||||
padding: '16px',
|
||||
borderRadius: '8px',
|
||||
fontSize: '13px',
|
||||
maxHeight: '250px',
|
||||
overflow: 'auto',
|
||||
border: '1px solid #e8e8e8',
|
||||
lineHeight: '1.5'
|
||||
}}>
|
||||
<Text strong style={{ display: "block", marginBottom: "8px", fontSize: "15px" }}>
|
||||
API Request
|
||||
</Text>
|
||||
<pre
|
||||
style={{
|
||||
backgroundColor: "#f5f5f5",
|
||||
padding: "16px",
|
||||
borderRadius: "8px",
|
||||
fontSize: "13px",
|
||||
maxHeight: "250px",
|
||||
overflow: "auto",
|
||||
border: "1px solid #e8e8e8",
|
||||
lineHeight: "1.5",
|
||||
}}
|
||||
>
|
||||
{curlCommand || "No request data available"}
|
||||
</pre>
|
||||
<Button
|
||||
style={{ marginTop: '8px' }}
|
||||
icon={<CopyOutlined />}
|
||||
<Button
|
||||
style={{ marginTop: "8px" }}
|
||||
icon={<CopyOutlined />}
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(curlCommand || '');
|
||||
NotificationsManager.success('Copied to clipboard');
|
||||
navigator.clipboard.writeText(curlCommand || "");
|
||||
NotificationsManager.success("Copied to clipboard");
|
||||
}}
|
||||
>
|
||||
Copy to Clipboard
|
||||
|
|
@ -240,14 +279,9 @@ ${formattedBody}
|
|||
</div>
|
||||
</>
|
||||
)}
|
||||
<Divider style={{ margin: '24px 0 16px' }} />
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Button
|
||||
type="link"
|
||||
href="https://docs.litellm.ai/docs/providers"
|
||||
target="_blank"
|
||||
icon={<InfoCircleOutlined />}
|
||||
>
|
||||
<Divider style={{ margin: "24px 0 16px" }} />
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||
<Button type="link" href="https://docs.litellm.ai/docs/providers" target="_blank" icon={<InfoCircleOutlined />}>
|
||||
View Documentation
|
||||
</Button>
|
||||
</div>
|
||||
|
|
@ -255,4 +289,4 @@ ${formattedBody}
|
|||
);
|
||||
};
|
||||
|
||||
export default ModelConnectionTest;
|
||||
export default ModelConnectionTest;
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import { provider_map, Providers } from "../provider_info_helpers";
|
|||
import { CredentialItem } from "../networking";
|
||||
const { Link } = Typography;
|
||||
|
||||
|
||||
interface ProviderSpecificFieldsProps {
|
||||
selectedProvider: Providers;
|
||||
uploadProps?: UploadProps;
|
||||
|
|
@ -29,13 +28,10 @@ export interface CredentialValues {
|
|||
value: string;
|
||||
}
|
||||
|
||||
|
||||
export const createCredentialFromModel = (provider: string, modelData: any): CredentialItem => {
|
||||
console.log("provider", provider);
|
||||
console.log("modelData", modelData);
|
||||
const enumKey = Object.keys(provider_map).find(
|
||||
key => provider_map[key].toLowerCase() === provider.toLowerCase()
|
||||
);
|
||||
const enumKey = Object.keys(provider_map).find((key) => provider_map[key].toLowerCase() === provider.toLowerCase());
|
||||
if (!enumKey) {
|
||||
throw new Error(`Provider ${provider} not found in provider_map`);
|
||||
}
|
||||
|
|
@ -46,7 +42,7 @@ export const createCredentialFromModel = (provider: string, modelData: any): Cre
|
|||
console.log("providerFields", providerFields);
|
||||
|
||||
// Go through each field defined for this provider
|
||||
providerFields.forEach(field => {
|
||||
providerFields.forEach((field) => {
|
||||
const value = modelData.litellm_params[field.key];
|
||||
console.log("field", field);
|
||||
console.log("value", value);
|
||||
|
|
@ -61,8 +57,8 @@ export const createCredentialFromModel = (provider: string, modelData: any): Cre
|
|||
credential_info: {
|
||||
custom_llm_provider: provider,
|
||||
description: `Credential for ${provider}. Created from model ${modelData.model_name}`,
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
return credential;
|
||||
};
|
||||
|
|
@ -73,66 +69,60 @@ const PROVIDER_CREDENTIAL_FIELDS: Record<Providers, ProviderCredentialField[]> =
|
|||
key: "api_base",
|
||||
label: "API Base",
|
||||
type: "select",
|
||||
options: [
|
||||
"https://api.openai.com/v1",
|
||||
"https://eu.api.openai.com"
|
||||
],
|
||||
defaultValue: "https://api.openai.com/v1"
|
||||
options: ["https://api.openai.com/v1", "https://eu.api.openai.com"],
|
||||
defaultValue: "https://api.openai.com/v1",
|
||||
},
|
||||
{
|
||||
key: "organization",
|
||||
label: "OpenAI Organization ID",
|
||||
placeholder: "[OPTIONAL] my-unique-org"
|
||||
placeholder: "[OPTIONAL] my-unique-org",
|
||||
},
|
||||
{
|
||||
key: "api_key",
|
||||
label: "OpenAI API Key",
|
||||
type: "password",
|
||||
required: true
|
||||
}
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
[Providers.OpenAI_Text]: [
|
||||
{
|
||||
key: "api_base",
|
||||
label: "API Base",
|
||||
type: "select",
|
||||
options: [
|
||||
"https://api.openai.com/v1",
|
||||
"https://eu.api.openai.com"
|
||||
],
|
||||
defaultValue: "https://api.openai.com/v1"
|
||||
options: ["https://api.openai.com/v1", "https://eu.api.openai.com"],
|
||||
defaultValue: "https://api.openai.com/v1",
|
||||
},
|
||||
{
|
||||
key: "organization",
|
||||
label: "OpenAI Organization ID",
|
||||
placeholder: "[OPTIONAL] my-unique-org"
|
||||
placeholder: "[OPTIONAL] my-unique-org",
|
||||
},
|
||||
{
|
||||
key: "api_key",
|
||||
label: "OpenAI API Key",
|
||||
type: "password",
|
||||
required: true
|
||||
}
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
[Providers.Vertex_AI]: [
|
||||
{
|
||||
key: "vertex_project",
|
||||
label: "Vertex Project",
|
||||
placeholder: "adroit-cadet-1234..",
|
||||
required: true
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
key: "vertex_location",
|
||||
label: "Vertex Location",
|
||||
placeholder: "us-east-1",
|
||||
required: true
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
key: "vertex_credentials",
|
||||
label: "Vertex Credentials",
|
||||
required: true,
|
||||
type: "upload"
|
||||
}
|
||||
type: "upload",
|
||||
},
|
||||
],
|
||||
[Providers.AssemblyAI]: [
|
||||
{
|
||||
|
|
@ -140,78 +130,77 @@ const PROVIDER_CREDENTIAL_FIELDS: Record<Providers, ProviderCredentialField[]> =
|
|||
label: "API Base",
|
||||
type: "select",
|
||||
required: true,
|
||||
options: [
|
||||
"https://api.assemblyai.com",
|
||||
"https://api.eu.assemblyai.com"
|
||||
]
|
||||
options: ["https://api.assemblyai.com", "https://api.eu.assemblyai.com"],
|
||||
},
|
||||
{
|
||||
key: "api_key",
|
||||
label: "AssemblyAI API Key",
|
||||
type: "password",
|
||||
required: true
|
||||
}
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
[Providers.Azure]: [
|
||||
{
|
||||
key: "api_base",
|
||||
label: "API Base",
|
||||
placeholder: "https://...",
|
||||
required: true
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
key: "api_version",
|
||||
label: "API Version",
|
||||
placeholder: "2023-07-01-preview",
|
||||
tooltip: "By default litellm will use the latest version. If you want to use a different version, you can specify it here"
|
||||
tooltip:
|
||||
"By default litellm will use the latest version. If you want to use a different version, you can specify it here",
|
||||
},
|
||||
{
|
||||
key: "base_model",
|
||||
label: "Base Model",
|
||||
placeholder: "azure/gpt-3.5-turbo"
|
||||
placeholder: "azure/gpt-3.5-turbo",
|
||||
},
|
||||
{
|
||||
key: "api_key",
|
||||
label: "Azure API Key",
|
||||
type: "password",
|
||||
required: true
|
||||
}
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
[Providers.Azure_AI_Studio]: [
|
||||
{
|
||||
key: "api_base",
|
||||
label: "API Base",
|
||||
placeholder: "https://<test>.openai.azure.com/openai/deployments/gpt-4o/chat/completions?api-version=2024-10-21",
|
||||
tooltip: "Enter your full Target URI from Azure Foundry here. Example: https://litellm8397336933.openai.azure.com/openai/deployments/gpt-4o/chat/completions?api-version=2024-10-21",
|
||||
required: true
|
||||
tooltip:
|
||||
"Enter your full Target URI from Azure Foundry here. Example: https://litellm8397336933.openai.azure.com/openai/deployments/gpt-4o/chat/completions?api-version=2024-10-21",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
key: "api_key",
|
||||
label: "Azure API Key",
|
||||
type: "password",
|
||||
required: true
|
||||
}
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
[Providers.OpenAI_Compatible]: [
|
||||
{
|
||||
key: "api_base",
|
||||
label: "API Base",
|
||||
placeholder: "https://...",
|
||||
required: true
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
key: "api_key",
|
||||
label: "OpenAI API Key",
|
||||
type: "password",
|
||||
required: true
|
||||
}
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
[Providers.Dashscope]: [
|
||||
{
|
||||
key: "api_key",
|
||||
label: "Dashscope API Key",
|
||||
type: "password",
|
||||
required: true
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
key: "api_base",
|
||||
|
|
@ -219,22 +208,23 @@ const PROVIDER_CREDENTIAL_FIELDS: Record<Providers, ProviderCredentialField[]> =
|
|||
placeholder: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
|
||||
defaultValue: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
|
||||
required: true,
|
||||
tooltip: "The base URL for your Dashscope server. Defaults to https://dashscope-intl.aliyuncs.com/compatible-mode/v1 if not specified."
|
||||
}
|
||||
tooltip:
|
||||
"The base URL for your Dashscope server. Defaults to https://dashscope-intl.aliyuncs.com/compatible-mode/v1 if not specified.",
|
||||
},
|
||||
],
|
||||
[Providers.OpenAI_Text_Compatible]: [
|
||||
{
|
||||
key: "api_base",
|
||||
label: "API Base",
|
||||
placeholder: "https://...",
|
||||
required: true
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
key: "api_key",
|
||||
label: "OpenAI API Key",
|
||||
type: "password",
|
||||
required: true
|
||||
}
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
[Providers.Bedrock]: [
|
||||
{
|
||||
|
|
@ -242,64 +232,70 @@ const PROVIDER_CREDENTIAL_FIELDS: Record<Providers, ProviderCredentialField[]> =
|
|||
label: "AWS Access Key ID",
|
||||
type: "password",
|
||||
required: false,
|
||||
tooltip: "You can provide the raw key or the environment variable (e.g. `os.environ/MY_SECRET_KEY`)."
|
||||
tooltip: "You can provide the raw key or the environment variable (e.g. `os.environ/MY_SECRET_KEY`).",
|
||||
},
|
||||
{
|
||||
key: "aws_secret_access_key",
|
||||
label: "AWS Secret Access Key",
|
||||
type: "password",
|
||||
required: false,
|
||||
tooltip: "You can provide the raw key or the environment variable (e.g. `os.environ/MY_SECRET_KEY`)."
|
||||
tooltip: "You can provide the raw key or the environment variable (e.g. `os.environ/MY_SECRET_KEY`).",
|
||||
},
|
||||
{
|
||||
key: "aws_session_token",
|
||||
label: "AWS Session Token",
|
||||
type: "password",
|
||||
required: false,
|
||||
tooltip: "Temporary credentials session token. You can provide the raw token or the environment variable (e.g. `os.environ/MY_SESSION_TOKEN`)."
|
||||
tooltip:
|
||||
"Temporary credentials session token. You can provide the raw token or the environment variable (e.g. `os.environ/MY_SESSION_TOKEN`).",
|
||||
},
|
||||
{
|
||||
key: "aws_region_name",
|
||||
label: "AWS Region Name",
|
||||
placeholder: "us-east-1",
|
||||
required: false,
|
||||
tooltip: "You can provide the raw key or the environment variable (e.g. `os.environ/MY_SECRET_KEY`)."
|
||||
tooltip: "You can provide the raw key or the environment variable (e.g. `os.environ/MY_SECRET_KEY`).",
|
||||
},
|
||||
{
|
||||
key: "aws_session_name",
|
||||
label: "AWS Session Name",
|
||||
placeholder: "my-session",
|
||||
required: false,
|
||||
tooltip: "Name for the AWS session. You can provide the raw value or the environment variable (e.g. `os.environ/MY_SESSION_NAME`)."
|
||||
tooltip:
|
||||
"Name for the AWS session. You can provide the raw value or the environment variable (e.g. `os.environ/MY_SESSION_NAME`).",
|
||||
},
|
||||
{
|
||||
key: "aws_profile_name",
|
||||
label: "AWS Profile Name",
|
||||
placeholder: "default",
|
||||
required: false,
|
||||
tooltip: "AWS profile name to use for authentication. You can provide the raw value or the environment variable (e.g. `os.environ/MY_PROFILE_NAME`)."
|
||||
tooltip:
|
||||
"AWS profile name to use for authentication. You can provide the raw value or the environment variable (e.g. `os.environ/MY_PROFILE_NAME`).",
|
||||
},
|
||||
{
|
||||
key: "aws_role_name",
|
||||
label: "AWS Role Name",
|
||||
placeholder: "MyRole",
|
||||
required: false,
|
||||
tooltip: "AWS IAM role name to assume. You can provide the raw value or the environment variable (e.g. `os.environ/MY_ROLE_NAME`)."
|
||||
tooltip:
|
||||
"AWS IAM role name to assume. You can provide the raw value or the environment variable (e.g. `os.environ/MY_ROLE_NAME`).",
|
||||
},
|
||||
{
|
||||
key: "aws_web_identity_token",
|
||||
label: "AWS Web Identity Token",
|
||||
type: "password",
|
||||
required: false,
|
||||
tooltip: "Web identity token for OIDC authentication. You can provide the raw token or the environment variable (e.g. `os.environ/MY_WEB_IDENTITY_TOKEN`)."
|
||||
tooltip:
|
||||
"Web identity token for OIDC authentication. You can provide the raw token or the environment variable (e.g. `os.environ/MY_WEB_IDENTITY_TOKEN`).",
|
||||
},
|
||||
{
|
||||
key: "aws_bedrock_runtime_endpoint",
|
||||
label: "AWS Bedrock Runtime Endpoint",
|
||||
placeholder: "https://bedrock-runtime.us-east-1.amazonaws.com",
|
||||
required: false,
|
||||
tooltip: "Custom Bedrock runtime endpoint URL. You can provide the raw value or the environment variable (e.g. `os.environ/MY_BEDROCK_ENDPOINT`)."
|
||||
}
|
||||
tooltip:
|
||||
"Custom Bedrock runtime endpoint URL. You can provide the raw value or the environment variable (e.g. `os.environ/MY_BEDROCK_ENDPOINT`).",
|
||||
},
|
||||
],
|
||||
[Providers.SageMaker]: [
|
||||
{
|
||||
|
|
@ -307,22 +303,22 @@ const PROVIDER_CREDENTIAL_FIELDS: Record<Providers, ProviderCredentialField[]> =
|
|||
label: "AWS Access Key ID",
|
||||
type: "password",
|
||||
required: false,
|
||||
tooltip: "You can provide the raw key or the environment variable (e.g. `os.environ/MY_SECRET_KEY`)."
|
||||
tooltip: "You can provide the raw key or the environment variable (e.g. `os.environ/MY_SECRET_KEY`).",
|
||||
},
|
||||
{
|
||||
key: "aws_secret_access_key",
|
||||
label: "AWS Secret Access Key",
|
||||
type: "password",
|
||||
required: false,
|
||||
tooltip: "You can provide the raw key or the environment variable (e.g. `os.environ/MY_SECRET_KEY`)."
|
||||
tooltip: "You can provide the raw key or the environment variable (e.g. `os.environ/MY_SECRET_KEY`).",
|
||||
},
|
||||
{
|
||||
key: "aws_region_name",
|
||||
label: "AWS Region Name",
|
||||
placeholder: "us-east-1",
|
||||
required: false,
|
||||
tooltip: "You can provide the raw key or the environment variable (e.g. `os.environ/MY_SECRET_KEY`)."
|
||||
}
|
||||
tooltip: "You can provide the raw key or the environment variable (e.g. `os.environ/MY_SECRET_KEY`).",
|
||||
},
|
||||
],
|
||||
[Providers.Ollama]: [
|
||||
{
|
||||
|
|
@ -331,202 +327,248 @@ const PROVIDER_CREDENTIAL_FIELDS: Record<Providers, ProviderCredentialField[]> =
|
|||
placeholder: "http://localhost:11434",
|
||||
defaultValue: "http://localhost:11434",
|
||||
required: false,
|
||||
tooltip: "The base URL for your Ollama server. Defaults to http://localhost:11434 if not specified."
|
||||
}
|
||||
tooltip: "The base URL for your Ollama server. Defaults to http://localhost:11434 if not specified.",
|
||||
},
|
||||
],
|
||||
[Providers.Anthropic]: [
|
||||
{
|
||||
key: "api_key",
|
||||
label: "API Key",
|
||||
placeholder: "sk-",
|
||||
type: "password",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
[Providers.Deepgram]: [
|
||||
{
|
||||
key: "api_key",
|
||||
label: "API Key",
|
||||
type: "password",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
[Providers.ElevenLabs]: [
|
||||
{
|
||||
key: "api_key",
|
||||
label: "API Key",
|
||||
type: "password",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
[Providers.Google_AI_Studio]: [
|
||||
{
|
||||
key: "api_key",
|
||||
label: "API Key",
|
||||
placeholder: "aig-",
|
||||
type: "password",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
[Providers.Groq]: [
|
||||
{
|
||||
key: "api_key",
|
||||
label: "API Key",
|
||||
type: "password",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
[Providers.MistralAI]: [
|
||||
{
|
||||
key: "api_key",
|
||||
label: "API Key",
|
||||
type: "password",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
[Providers.Deepseek]: [
|
||||
{
|
||||
key: "api_key",
|
||||
label: "API Key",
|
||||
type: "password",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
[Providers.Cohere]: [
|
||||
{
|
||||
key: "api_key",
|
||||
label: "API Key",
|
||||
type: "password",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
[Providers.Databricks]: [
|
||||
{
|
||||
key: "api_key",
|
||||
label: "API Key",
|
||||
type: "password",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
[Providers.xAI]: [
|
||||
{
|
||||
key: "api_key",
|
||||
label: "API Key",
|
||||
type: "password",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
[Providers.AIML]: [
|
||||
{
|
||||
key: "api_key",
|
||||
label: "API Key",
|
||||
type: "password",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
[Providers.Cerebras]: [
|
||||
{
|
||||
key: "api_key",
|
||||
label: "API Key",
|
||||
type: "password",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
[Providers.Sambanova]: [
|
||||
{
|
||||
key: "api_key",
|
||||
label: "API Key",
|
||||
type: "password",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
[Providers.Perplexity]: [
|
||||
{
|
||||
key: "api_key",
|
||||
label: "API Key",
|
||||
type: "password",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
[Providers.TogetherAI]: [
|
||||
{
|
||||
key: "api_key",
|
||||
label: "API Key",
|
||||
type: "password",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
[Providers.Openrouter]: [
|
||||
{
|
||||
key: "api_key",
|
||||
label: "API Key",
|
||||
type: "password",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
[Providers.FireworksAI]: [
|
||||
{
|
||||
key: "api_key",
|
||||
label: "API Key",
|
||||
type: "password",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
[Providers.Anthropic]: [{
|
||||
key: "api_key",
|
||||
label: "API Key",
|
||||
placeholder: "sk-",
|
||||
type: "password",
|
||||
required: true
|
||||
}],
|
||||
[Providers.Deepgram]: [{
|
||||
key: "api_key",
|
||||
label: "API Key",
|
||||
type: "password",
|
||||
required: true
|
||||
}],
|
||||
[Providers.ElevenLabs]: [{
|
||||
key: "api_key",
|
||||
label: "API Key",
|
||||
type: "password",
|
||||
required: true
|
||||
}],
|
||||
[Providers.Google_AI_Studio]: [{
|
||||
key: "api_key",
|
||||
label: "API Key",
|
||||
placeholder: "aig-",
|
||||
type: "password",
|
||||
required: true
|
||||
}],
|
||||
[Providers.Groq]: [{
|
||||
key: "api_key",
|
||||
label: "API Key",
|
||||
type: "password",
|
||||
required: true
|
||||
}],
|
||||
[Providers.MistralAI]: [{
|
||||
key: "api_key",
|
||||
label: "API Key",
|
||||
type: "password",
|
||||
required: true
|
||||
}],
|
||||
[Providers.Deepseek]: [{
|
||||
key: "api_key",
|
||||
label: "API Key",
|
||||
type: "password",
|
||||
required: true
|
||||
}],
|
||||
[Providers.Cohere]: [{
|
||||
key: "api_key",
|
||||
label: "API Key",
|
||||
type: "password",
|
||||
required: true
|
||||
}],
|
||||
[Providers.Databricks]: [{
|
||||
key: "api_key",
|
||||
label: "API Key",
|
||||
type: "password",
|
||||
required: true
|
||||
}],
|
||||
[Providers.xAI]: [{
|
||||
key: "api_key",
|
||||
label: "API Key",
|
||||
type: "password",
|
||||
required: true
|
||||
}],
|
||||
[Providers.AIML]: [{
|
||||
key: "api_key",
|
||||
label: "API Key",
|
||||
type: "password",
|
||||
required: true
|
||||
}],
|
||||
[Providers.Cerebras]: [{
|
||||
key: "api_key",
|
||||
label: "API Key",
|
||||
type: "password",
|
||||
required: true
|
||||
}],
|
||||
[Providers.Sambanova]: [{
|
||||
key: "api_key",
|
||||
label: "API Key",
|
||||
type: "password",
|
||||
required: true
|
||||
}],
|
||||
[Providers.Perplexity]: [{
|
||||
key: "api_key",
|
||||
label: "API Key",
|
||||
type: "password",
|
||||
required: true
|
||||
}],
|
||||
[Providers.TogetherAI]: [{
|
||||
key: "api_key",
|
||||
label: "API Key",
|
||||
type: "password",
|
||||
required: true
|
||||
}],
|
||||
[Providers.Openrouter]: [{
|
||||
key: "api_key",
|
||||
label: "API Key",
|
||||
type: "password",
|
||||
required: true
|
||||
}],
|
||||
[Providers.FireworksAI]: [{
|
||||
key: "api_key",
|
||||
label: "API Key",
|
||||
type: "password",
|
||||
required: true
|
||||
}],
|
||||
[Providers.GradientAI]: [
|
||||
{
|
||||
key: "api_base",
|
||||
label: "GradientAI Endpoint",
|
||||
placeholder: "https://...",
|
||||
required: false
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
key: "api_key",
|
||||
label: "GradientAI API Key",
|
||||
type: "password",
|
||||
required: true
|
||||
}
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
[Providers.Triton]: [
|
||||
{
|
||||
key: "api_key",
|
||||
label: "API Key",
|
||||
type: "password",
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
key: "api_base",
|
||||
label: "API Base",
|
||||
placeholder: "http://localhost:8000/generate",
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
[Providers.Triton]: [{
|
||||
key: "api_key",
|
||||
label: "API Key",
|
||||
type: "password",
|
||||
required: false
|
||||
},
|
||||
{
|
||||
key: "api_base",
|
||||
label: "API Base",
|
||||
placeholder: "http://localhost:8000/generate",
|
||||
required: false
|
||||
}],
|
||||
[Providers.Hosted_Vllm]: [
|
||||
{
|
||||
key: "api_base",
|
||||
label: "API Base",
|
||||
placeholder: "https://...",
|
||||
required: true
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
key: "api_key",
|
||||
label: "OpenAI API Key",
|
||||
type: "password",
|
||||
required: true
|
||||
}
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
[Providers.Voyage]: [
|
||||
{
|
||||
key: "api_key",
|
||||
label: "API Key",
|
||||
type: "password",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
[Providers.JinaAI]: [
|
||||
{
|
||||
key: "api_key",
|
||||
label: "API Key",
|
||||
type: "password",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
[Providers.VolcEngine]: [
|
||||
{
|
||||
key: "api_key",
|
||||
label: "API Key",
|
||||
type: "password",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
[Providers.DeepInfra]: [
|
||||
{
|
||||
key: "api_key",
|
||||
label: "API Key",
|
||||
type: "password",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
[Providers.Oracle]: [
|
||||
{
|
||||
key: "api_key",
|
||||
label: "API Key",
|
||||
type: "password",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
[Providers.Snowflake]: [
|
||||
{
|
||||
key: "api_key",
|
||||
label: "Snowflake API Key / JWT Key for Authentication",
|
||||
type: "password",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
key: "api_base",
|
||||
label: "Snowflake API Endpoint",
|
||||
placeholder: "https://1234567890.snowflakecomputing.com/api/v2/cortex/inference:complete",
|
||||
tooltip:
|
||||
"Enter the full endpoint with path here. Example: https://1234567890.snowflakecomputing.com/api/v2/cortex/inference:complete",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
[Providers.Voyage]: [{
|
||||
key: "api_key",
|
||||
label: "API Key",
|
||||
type: "password",
|
||||
required: true
|
||||
}],
|
||||
[Providers.JinaAI]: [{
|
||||
key: "api_key",
|
||||
label: "API Key",
|
||||
type: "password",
|
||||
required: true
|
||||
}],
|
||||
[Providers.VolcEngine]: [{
|
||||
key: "api_key",
|
||||
label: "API Key",
|
||||
type: "password",
|
||||
required: true
|
||||
}],
|
||||
[Providers.DeepInfra]: [{
|
||||
key: "api_key",
|
||||
label: "API Key",
|
||||
type: "password",
|
||||
required: true
|
||||
}],
|
||||
[Providers.Oracle]: [{
|
||||
key: "api_key",
|
||||
label: "API Key",
|
||||
type: "password",
|
||||
required: true
|
||||
}],
|
||||
[Providers.Snowflake]: [{
|
||||
key: "api_key",
|
||||
label: "Snowflake API Key / JWT Key for Authentication",
|
||||
type: "password",
|
||||
required: true
|
||||
},
|
||||
{
|
||||
key: "api_base",
|
||||
label: "Snowflake API Endpoint",
|
||||
placeholder: "https://1234567890.snowflakecomputing.com/api/v2/cortex/inference:complete",
|
||||
tooltip: "Enter the full endpoint with path here. Example: https://1234567890.snowflakecomputing.com/api/v2/cortex/inference:complete",
|
||||
required: true
|
||||
}]
|
||||
};
|
||||
|
||||
const ProviderSpecificFields: React.FC<ProviderSpecificFieldsProps> = ({
|
||||
selectedProvider,
|
||||
uploadProps
|
||||
}) => {
|
||||
const ProviderSpecificFields: React.FC<ProviderSpecificFieldsProps> = ({ selectedProvider, uploadProps }) => {
|
||||
const selectedProviderEnum = Providers[selectedProvider as keyof typeof Providers] as Providers;
|
||||
const form = Form.useFormInstance(); // Get form instance from context
|
||||
|
||||
|
|
@ -561,7 +603,7 @@ const ProviderSpecificFields: React.FC<ProviderSpecificFieldsProps> = ({
|
|||
if (info.file.status !== "uploading") {
|
||||
console.log(info.file, info.fileList);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -576,10 +618,7 @@ const ProviderSpecificFields: React.FC<ProviderSpecificFieldsProps> = ({
|
|||
className={field.key === "vertex_credentials" ? "mb-0" : undefined}
|
||||
>
|
||||
{field.type === "select" ? (
|
||||
<Select
|
||||
placeholder={field.placeholder}
|
||||
defaultValue={field.defaultValue}
|
||||
>
|
||||
<Select placeholder={field.placeholder} defaultValue={field.defaultValue}>
|
||||
{field.options?.map((option) => (
|
||||
<Select.Option key={option} value={option}>
|
||||
{option}
|
||||
|
|
@ -605,10 +644,7 @@ const ProviderSpecificFields: React.FC<ProviderSpecificFieldsProps> = ({
|
|||
<Button2 icon={<UploadOutlined />}>Click to Upload</Button2>
|
||||
</Upload>
|
||||
) : (
|
||||
<TextInput
|
||||
placeholder={field.placeholder}
|
||||
type={field.type === "password" ? "password" : "text"}
|
||||
/>
|
||||
<TextInput placeholder={field.placeholder} type={field.type === "password" ? "password" : "text"} />
|
||||
)}
|
||||
</Form.Item>
|
||||
|
||||
|
|
@ -616,9 +652,7 @@ const ProviderSpecificFields: React.FC<ProviderSpecificFieldsProps> = ({
|
|||
{field.key === "vertex_credentials" && (
|
||||
<Row>
|
||||
<Col>
|
||||
<Text className="mb-3 mt-1">
|
||||
Give a gcp service account(.json file)
|
||||
</Text>
|
||||
<Text className="mb-3 mt-1">Give a gcp service account(.json file)</Text>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
|
@ -629,8 +663,7 @@ const ProviderSpecificFields: React.FC<ProviderSpecificFieldsProps> = ({
|
|||
<Col span={10}></Col>
|
||||
<Col span={10}>
|
||||
<Text className="mb-2">
|
||||
The actual model your azure deployment uses. Used
|
||||
for accurate cost tracking. Select name from{" "}
|
||||
The actual model your azure deployment uses. Used for accurate cost tracking. Select name from{" "}
|
||||
<Link
|
||||
href="https://github.com/BerriAI/litellm/blob/main/model_prices_and_context_window.json"
|
||||
target="_blank"
|
||||
|
|
|
|||
|
|
@ -34,11 +34,7 @@ interface RouterConfigBuilderProps {
|
|||
onChange?: (config: any) => void;
|
||||
}
|
||||
|
||||
const RouterConfigBuilder: React.FC<RouterConfigBuilderProps> = ({
|
||||
modelInfo,
|
||||
value,
|
||||
onChange,
|
||||
}) => {
|
||||
const RouterConfigBuilder: React.FC<RouterConfigBuilderProps> = ({ modelInfo, value, onChange }) => {
|
||||
const [routes, setRoutes] = useState<Route[]>([]);
|
||||
const [showJsonPreview, setShowJsonPreview] = useState<boolean>(false);
|
||||
const [expandedRoutes, setExpandedRoutes] = useState<string[]>([]);
|
||||
|
|
@ -54,9 +50,9 @@ const RouterConfigBuilder: React.FC<RouterConfigBuilderProps> = ({
|
|||
score_threshold: route.score_threshold || 0.5,
|
||||
}));
|
||||
setRoutes(initializedRoutes);
|
||||
|
||||
|
||||
// Set expanded routes for existing routes
|
||||
const routeIds = initializedRoutes.map(route => route.id);
|
||||
const routeIds = initializedRoutes.map((route) => route.id);
|
||||
setExpandedRoutes(routeIds);
|
||||
} else {
|
||||
setRoutes([]);
|
||||
|
|
@ -78,23 +74,21 @@ const RouterConfigBuilder: React.FC<RouterConfigBuilderProps> = ({
|
|||
setRoutes(updatedRoutes);
|
||||
updateConfig(updatedRoutes);
|
||||
// Automatically expand the new route
|
||||
setExpandedRoutes(prev => [...prev, newRouteId]);
|
||||
setExpandedRoutes((prev) => [...prev, newRouteId]);
|
||||
};
|
||||
|
||||
// Handle removing a route
|
||||
const removeRoute = (routeId: string) => {
|
||||
const updatedRoutes = routes.filter(route => route.id !== routeId);
|
||||
const updatedRoutes = routes.filter((route) => route.id !== routeId);
|
||||
setRoutes(updatedRoutes);
|
||||
updateConfig(updatedRoutes);
|
||||
// Remove from expanded routes as well
|
||||
setExpandedRoutes(prev => prev.filter(id => id !== routeId));
|
||||
setExpandedRoutes((prev) => prev.filter((id) => id !== routeId));
|
||||
};
|
||||
|
||||
// Handle updating a route
|
||||
const updateRoute = (routeId: string, field: keyof Route, value: any) => {
|
||||
const updatedRoutes = routes.map(route =>
|
||||
route.id === routeId ? { ...route, [field]: value } : route
|
||||
);
|
||||
const updatedRoutes = routes.map((route) => (route.id === routeId ? { ...route, [field]: value } : route));
|
||||
setRoutes(updatedRoutes);
|
||||
updateConfig(updatedRoutes);
|
||||
};
|
||||
|
|
@ -102,7 +96,7 @@ const RouterConfigBuilder: React.FC<RouterConfigBuilderProps> = ({
|
|||
// Update the overall configuration
|
||||
const updateConfig = (updatedRoutes: Route[]) => {
|
||||
const config = {
|
||||
routes: updatedRoutes.map(route => ({
|
||||
routes: updatedRoutes.map((route) => ({
|
||||
name: route.model,
|
||||
utterances: route.utterances,
|
||||
description: route.description,
|
||||
|
|
@ -115,21 +109,21 @@ const RouterConfigBuilder: React.FC<RouterConfigBuilderProps> = ({
|
|||
// Handle utterances change (convert textarea string to array)
|
||||
const handleUtterancesChange = (routeId: string, utterancesText: string) => {
|
||||
const utterancesArray = utterancesText
|
||||
.split('\n')
|
||||
.map(line => line.trim()) // Only trims leading/trailing whitespace, preserves internal spaces
|
||||
.filter(line => line.length > 0);
|
||||
updateRoute(routeId, 'utterances', utterancesArray);
|
||||
.split("\n")
|
||||
.map((line) => line.trim()) // Only trims leading/trailing whitespace, preserves internal spaces
|
||||
.filter((line) => line.length > 0);
|
||||
updateRoute(routeId, "utterances", utterancesArray);
|
||||
};
|
||||
|
||||
// Prepare model options for dropdowns
|
||||
const modelOptions = modelInfo.map(model => ({
|
||||
const modelOptions = modelInfo.map((model) => ({
|
||||
value: model.model_group,
|
||||
label: model.model_group,
|
||||
}));
|
||||
|
||||
const generateConfig = () => {
|
||||
return {
|
||||
routes: routes.map(route => ({
|
||||
routes: routes.map((route) => ({
|
||||
name: route.model,
|
||||
utterances: route.utterances,
|
||||
description: route.description,
|
||||
|
|
@ -148,12 +142,7 @@ const RouterConfigBuilder: React.FC<RouterConfigBuilderProps> = ({
|
|||
<InfoCircleOutlined className="text-gray-400" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={addRoute}
|
||||
className="bg-blue-600 hover:bg-blue-700"
|
||||
>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={addRoute} className="bg-blue-600 hover:bg-blue-700">
|
||||
Add Route
|
||||
</Button>
|
||||
</div>
|
||||
|
|
@ -166,11 +155,7 @@ const RouterConfigBuilder: React.FC<RouterConfigBuilderProps> = ({
|
|||
) : (
|
||||
<div className="space-y-3 mb-6 w-full">
|
||||
{routes.map((route, index) => (
|
||||
<Card
|
||||
key={route.id}
|
||||
className="border border-gray-200 shadow-sm w-full"
|
||||
bodyStyle={{ padding: 0 }}
|
||||
>
|
||||
<Card key={route.id} className="border border-gray-200 shadow-sm w-full" bodyStyle={{ padding: 0 }}>
|
||||
<Collapse
|
||||
ghost
|
||||
expandIcon={({ isActive }) => <DownOutlined rotate={isActive ? 180 : 0} />}
|
||||
|
|
@ -182,11 +167,11 @@ const RouterConfigBuilder: React.FC<RouterConfigBuilderProps> = ({
|
|||
label: (
|
||||
<div className="flex justify-between items-center py-2">
|
||||
<Text className="font-medium text-base">
|
||||
Route {index + 1}: {route.model || 'Unnamed'}
|
||||
Route {index + 1}: {route.model || "Unnamed"}
|
||||
</Text>
|
||||
<Button
|
||||
type="text"
|
||||
danger
|
||||
<Button
|
||||
type="text"
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
|
|
@ -203,10 +188,10 @@ const RouterConfigBuilder: React.FC<RouterConfigBuilderProps> = ({
|
|||
<Text className="text-sm font-medium mb-2 block">Model</Text>
|
||||
<AntdSelect
|
||||
value={route.model}
|
||||
onChange={(value) => updateRoute(route.id, 'model', value)}
|
||||
onChange={(value) => updateRoute(route.id, "model", value)}
|
||||
placeholder="Select model"
|
||||
showSearch
|
||||
style={{ width: '100%' }}
|
||||
style={{ width: "100%" }}
|
||||
options={modelOptions}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -216,10 +201,10 @@ const RouterConfigBuilder: React.FC<RouterConfigBuilderProps> = ({
|
|||
<Text className="text-sm font-medium mb-2 block">Description</Text>
|
||||
<TextArea
|
||||
value={route.description}
|
||||
onChange={(e) => updateRoute(route.id, 'description', e.target.value)}
|
||||
onChange={(e) => updateRoute(route.id, "description", e.target.value)}
|
||||
placeholder="Describe when this route should be used..."
|
||||
rows={2}
|
||||
style={{ width: '100%' }}
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -233,11 +218,11 @@ const RouterConfigBuilder: React.FC<RouterConfigBuilderProps> = ({
|
|||
</div>
|
||||
<InputNumber
|
||||
value={route.score_threshold}
|
||||
onChange={(value) => updateRoute(route.id, 'score_threshold', value || 0)}
|
||||
onChange={(value) => updateRoute(route.id, "score_threshold", value || 0)}
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.1}
|
||||
style={{ width: '100%' }}
|
||||
style={{ width: "100%" }}
|
||||
placeholder="0.5"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -250,14 +235,16 @@ const RouterConfigBuilder: React.FC<RouterConfigBuilderProps> = ({
|
|||
<InfoCircleOutlined className="text-gray-400" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Text className="text-xs text-gray-500 mb-2">Type an utterance and press Enter to add it. You can also paste multiple lines.</Text>
|
||||
<Text className="text-xs text-gray-500 mb-2">
|
||||
Type an utterance and press Enter to add it. You can also paste multiple lines.
|
||||
</Text>
|
||||
<AntdSelect
|
||||
mode="tags"
|
||||
value={route.utterances}
|
||||
onChange={(utterances) => updateRoute(route.id, 'utterances', utterances)}
|
||||
onChange={(utterances) => updateRoute(route.id, "utterances", utterances)}
|
||||
placeholder="Type an utterance and press Enter..."
|
||||
style={{ width: '100%' }}
|
||||
tokenSeparators={['\n']}
|
||||
style={{ width: "100%" }}
|
||||
tokenSeparators={["\n"]}
|
||||
maxTagCount="responsive"
|
||||
allowClear
|
||||
/>
|
||||
|
|
@ -276,20 +263,14 @@ const RouterConfigBuilder: React.FC<RouterConfigBuilderProps> = ({
|
|||
<div className="border-t pt-6 w-full">
|
||||
<div className="flex justify-between items-center mb-4 w-full">
|
||||
<Text className="text-lg font-semibold">JSON Preview</Text>
|
||||
<Button
|
||||
type="link"
|
||||
onClick={() => setShowJsonPreview(!showJsonPreview)}
|
||||
className="text-blue-600 p-0"
|
||||
>
|
||||
{showJsonPreview ? 'Hide' : 'Show'}
|
||||
<Button type="link" onClick={() => setShowJsonPreview(!showJsonPreview)} className="text-blue-600 p-0">
|
||||
{showJsonPreview ? "Hide" : "Show"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
||||
{showJsonPreview && (
|
||||
<Card className="bg-gray-50 w-full">
|
||||
<pre className="text-sm overflow-auto max-h-64 w-full">
|
||||
{JSON.stringify(generateConfig(), null, 2)}
|
||||
</pre>
|
||||
<pre className="text-sm overflow-auto max-h-64 w-full">{JSON.stringify(generateConfig(), null, 2)}</pre>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -297,4 +278,4 @@ const RouterConfigBuilder: React.FC<RouterConfigBuilderProps> = ({
|
|||
);
|
||||
};
|
||||
|
||||
export default RouterConfigBuilder;
|
||||
export default RouterConfigBuilder;
|
||||
|
|
|
|||
|
|
@ -2,11 +2,22 @@
|
|||
* Modal to add fallbacks to the proxy router config
|
||||
*/
|
||||
|
||||
|
||||
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { Button, TextInput, Grid, Col, Switch } from "@tremor/react";
|
||||
import { Select, SelectItem, MultiSelect, MultiSelectItem, Card, Metric, Text, Title, Subtitle, Accordion, AccordionHeader, AccordionBody, } from "@tremor/react";
|
||||
import {
|
||||
Select,
|
||||
SelectItem,
|
||||
MultiSelect,
|
||||
MultiSelectItem,
|
||||
Card,
|
||||
Metric,
|
||||
Text,
|
||||
Title,
|
||||
Subtitle,
|
||||
Accordion,
|
||||
AccordionHeader,
|
||||
AccordionBody,
|
||||
} from "@tremor/react";
|
||||
import { createPassThroughEndpoint } from "./networking";
|
||||
import {
|
||||
Button as Button2,
|
||||
|
|
@ -22,7 +33,13 @@ import {
|
|||
Collapse,
|
||||
} from "antd";
|
||||
import NumericalInput from "./shared/numerical_input";
|
||||
import { InfoCircleOutlined, ApiOutlined, ExclamationCircleOutlined, CheckCircleOutlined, CopyOutlined } from "@ant-design/icons";
|
||||
import {
|
||||
InfoCircleOutlined,
|
||||
ApiOutlined,
|
||||
ExclamationCircleOutlined,
|
||||
CheckCircleOutlined,
|
||||
CopyOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { keyCreateCall, slackBudgetAlertsHealthCheck, modelAvailableCall } from "./networking";
|
||||
import { list } from "postcss";
|
||||
import KeyValueInput from "./key_value_input";
|
||||
|
|
@ -32,14 +49,16 @@ import NotificationsManager from "./molecules/notifications_manager";
|
|||
const { Option } = Select2;
|
||||
|
||||
interface AddFallbacksProps {
|
||||
// models: string[] | undefined;
|
||||
// models: string[] | undefined;
|
||||
accessToken: string;
|
||||
passThroughItems: passThroughItem[];
|
||||
setPassThroughItems: React.Dispatch<React.SetStateAction<passThroughItem[]>>;
|
||||
}
|
||||
|
||||
const AddPassThroughEndpoint: React.FC<AddFallbacksProps> = ({
|
||||
accessToken, setPassThroughItems, passThroughItems
|
||||
accessToken,
|
||||
setPassThroughItems,
|
||||
passThroughItems,
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
|
|
@ -60,8 +79,8 @@ const AddPassThroughEndpoint: React.FC<AddFallbacksProps> = ({
|
|||
const handlePathChange = (value: string) => {
|
||||
// Auto-add leading slash if missing
|
||||
let formattedPath = value;
|
||||
if (value && !value.startsWith('/')) {
|
||||
formattedPath = '/' + value;
|
||||
if (value && !value.startsWith("/")) {
|
||||
formattedPath = "/" + value;
|
||||
}
|
||||
setPathValue(formattedPath);
|
||||
form.setFieldsValue({ path: formattedPath });
|
||||
|
|
@ -74,13 +93,13 @@ const AddPassThroughEndpoint: React.FC<AddFallbacksProps> = ({
|
|||
console.log(`formValues: ${JSON.stringify(formValues)}`);
|
||||
|
||||
const response = await createPassThroughEndpoint(accessToken, formValues);
|
||||
|
||||
|
||||
// Use the created endpoint from the API response (includes the generated ID)
|
||||
const createdEndpoint = response.endpoints[0];
|
||||
|
||||
const updatedPassThroughSettings = [...passThroughItems, createdEndpoint]
|
||||
setPassThroughItems(updatedPassThroughSettings)
|
||||
|
||||
|
||||
const updatedPassThroughSettings = [...passThroughItems, createdEndpoint];
|
||||
setPassThroughItems(updatedPassThroughSettings);
|
||||
|
||||
NotificationsManager.success("Pass-through endpoint created successfully");
|
||||
form.resetFields();
|
||||
setPathValue("");
|
||||
|
|
@ -96,17 +115,12 @@ const AddPassThroughEndpoint: React.FC<AddFallbacksProps> = ({
|
|||
|
||||
const copyToClipboard = (text: string) => {
|
||||
navigator.clipboard.writeText(text);
|
||||
NotificationsManager.success('Copied to clipboard!');
|
||||
NotificationsManager.success("Copied to clipboard!");
|
||||
};
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Button
|
||||
className="mx-auto mb-4 mt-4"
|
||||
onClick={() => setIsModalVisible(true)}
|
||||
>
|
||||
<Button className="mx-auto mb-4 mt-4" onClick={() => setIsModalVisible(true)}>
|
||||
+ Add Pass-Through Endpoint
|
||||
</Button>
|
||||
<Modal
|
||||
|
|
@ -122,8 +136,8 @@ const AddPassThroughEndpoint: React.FC<AddFallbacksProps> = ({
|
|||
footer={null}
|
||||
className="top-8"
|
||||
styles={{
|
||||
body: { padding: '24px' },
|
||||
header: { padding: '24px 24px 0 24px', border: 'none' },
|
||||
body: { padding: "24px" },
|
||||
header: { padding: "24px 24px 0 24px", border: "none" },
|
||||
}}
|
||||
>
|
||||
<div className="mt-6">
|
||||
|
|
@ -140,38 +154,32 @@ const AddPassThroughEndpoint: React.FC<AddFallbacksProps> = ({
|
|||
onFinish={addPassThrough}
|
||||
layout="vertical"
|
||||
className="space-y-6"
|
||||
initialValues={{
|
||||
initialValues={{
|
||||
include_subpath: true,
|
||||
path: pathValue,
|
||||
target: targetValue
|
||||
target: targetValue,
|
||||
}}
|
||||
>
|
||||
{/* Route Configuration Section */}
|
||||
<Card className="p-5">
|
||||
<Title className="text-lg font-semibold text-gray-900 mb-2">Route Configuration</Title>
|
||||
<Subtitle className="text-gray-600 mb-5">Configure how requests to your domain will be forwarded to the target API</Subtitle>
|
||||
|
||||
<Subtitle className="text-gray-600 mb-5">
|
||||
Configure how requests to your domain will be forwarded to the target API
|
||||
</Subtitle>
|
||||
|
||||
<div className="space-y-5">
|
||||
<Form.Item
|
||||
label={
|
||||
<span className="text-sm font-medium text-gray-700">
|
||||
Path Prefix
|
||||
</span>
|
||||
}
|
||||
label={<span className="text-sm font-medium text-gray-700">Path Prefix</span>}
|
||||
name="path"
|
||||
rules={[
|
||||
{ required: true, message: 'Path is required', pattern: /^\// }
|
||||
]}
|
||||
rules={[{ required: true, message: "Path is required", pattern: /^\// }]}
|
||||
extra={
|
||||
<div className="text-xs text-gray-500 mt-1">
|
||||
Example: /bria, /adobe-photoshop, /elasticsearch
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 mt-1">Example: /bria, /adobe-photoshop, /elasticsearch</div>
|
||||
}
|
||||
className="mb-4"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<TextInput
|
||||
placeholder="bria"
|
||||
<TextInput
|
||||
placeholder="bria"
|
||||
value={pathValue}
|
||||
onChange={(e) => handlePathChange(e.target.value)}
|
||||
className="flex-1"
|
||||
|
|
@ -180,25 +188,17 @@ const AddPassThroughEndpoint: React.FC<AddFallbacksProps> = ({
|
|||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={
|
||||
<span className="text-sm font-medium text-gray-700">
|
||||
Target URL
|
||||
</span>
|
||||
}
|
||||
label={<span className="text-sm font-medium text-gray-700">Target URL</span>}
|
||||
name="target"
|
||||
rules={[
|
||||
{ required: true, message: 'Target URL is required' },
|
||||
{ type: 'url', message: 'Please enter a valid URL' }
|
||||
{ required: true, message: "Target URL is required" },
|
||||
{ type: "url", message: "Please enter a valid URL" },
|
||||
]}
|
||||
extra={
|
||||
<div className="text-xs text-gray-500 mt-1">
|
||||
Example:https://engine.prod.bria-api.com
|
||||
</div>
|
||||
}
|
||||
extra={<div className="text-xs text-gray-500 mt-1">Example:https://engine.prod.bria-api.com</div>}
|
||||
className="mb-4"
|
||||
>
|
||||
<TextInput
|
||||
placeholder="https://engine.prod.bria-api.com"
|
||||
<TextInput
|
||||
placeholder="https://engine.prod.bria-api.com"
|
||||
value={targetValue}
|
||||
onChange={(e) => {
|
||||
setTargetValue(e.target.value);
|
||||
|
|
@ -210,34 +210,27 @@ const AddPassThroughEndpoint: React.FC<AddFallbacksProps> = ({
|
|||
<div className="flex items-center justify-between py-3">
|
||||
<div>
|
||||
<div className="text-sm font-medium text-gray-700">Include Subpaths</div>
|
||||
<div className="text-xs text-gray-500 mt-0.5">Forward all subpaths to the target API (recommended for REST APIs)</div>
|
||||
<div className="text-xs text-gray-500 mt-0.5">
|
||||
Forward all subpaths to the target API (recommended for REST APIs)
|
||||
</div>
|
||||
</div>
|
||||
<Form.Item
|
||||
name="include_subpath"
|
||||
valuePropName="checked"
|
||||
className="mb-0"
|
||||
>
|
||||
<Switch
|
||||
checked={includeSubpath}
|
||||
onChange={setIncludeSubpath}
|
||||
/>
|
||||
<Form.Item name="include_subpath" valuePropName="checked" className="mb-0">
|
||||
<Switch checked={includeSubpath} onChange={setIncludeSubpath} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Route Preview Section */}
|
||||
<RoutePreview
|
||||
pathValue={pathValue}
|
||||
targetValue={targetValue}
|
||||
includeSubpath={includeSubpath}
|
||||
/>
|
||||
<RoutePreview pathValue={pathValue} targetValue={targetValue} includeSubpath={includeSubpath} />
|
||||
|
||||
{/* Headers Section */}
|
||||
<Card className="p-6">
|
||||
<Title className="text-lg font-semibold text-gray-900 mb-2">Headers</Title>
|
||||
<Subtitle className="text-gray-600 mb-6">Add headers that will be sent with every request to the target API</Subtitle>
|
||||
|
||||
<Subtitle className="text-gray-600 mb-6">
|
||||
Add headers that will be sent with every request to the target API
|
||||
</Subtitle>
|
||||
|
||||
<Form.Item
|
||||
label={
|
||||
<span className="text-sm font-medium text-gray-700 flex items-center">
|
||||
|
|
@ -248,7 +241,7 @@ const AddPassThroughEndpoint: React.FC<AddFallbacksProps> = ({
|
|||
</span>
|
||||
}
|
||||
name="headers"
|
||||
rules={[{ required: true, message: 'Please configure the headers' }]}
|
||||
rules={[{ required: true, message: "Please configure the headers" }]}
|
||||
extra={
|
||||
<div className="text-xs text-gray-500 mt-2">
|
||||
<div className="font-medium mb-1">Add authentication tokens and other required headers</div>
|
||||
|
|
@ -256,7 +249,7 @@ const AddPassThroughEndpoint: React.FC<AddFallbacksProps> = ({
|
|||
</div>
|
||||
}
|
||||
>
|
||||
<KeyValueInput/>
|
||||
<KeyValueInput />
|
||||
</Form.Item>
|
||||
</Card>
|
||||
|
||||
|
|
@ -264,7 +257,7 @@ const AddPassThroughEndpoint: React.FC<AddFallbacksProps> = ({
|
|||
<Card className="p-6">
|
||||
<Title className="text-lg font-semibold text-gray-900 mb-2">Billing</Title>
|
||||
<Subtitle className="text-gray-600 mb-6">Optional cost tracking for this endpoint</Subtitle>
|
||||
|
||||
|
||||
<Form.Item
|
||||
label={
|
||||
<span className="text-sm font-medium text-gray-700 flex items-center">
|
||||
|
|
@ -281,24 +274,15 @@ const AddPassThroughEndpoint: React.FC<AddFallbacksProps> = ({
|
|||
</div>
|
||||
}
|
||||
>
|
||||
<NumericalInput
|
||||
min={0}
|
||||
step={0.001}
|
||||
precision={4}
|
||||
placeholder="2.0000"
|
||||
size="large"
|
||||
/>
|
||||
<NumericalInput min={0} step={0.001} precision={4} placeholder="2.0000" size="large" />
|
||||
</Form.Item>
|
||||
</Card>
|
||||
|
||||
<div className="flex items-center justify-end space-x-3 pt-6 border-t border-gray-100">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={handleCancel}
|
||||
>
|
||||
<Button variant="secondary" onClick={handleCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
<Button
|
||||
variant="primary"
|
||||
loading={isLoading}
|
||||
onClick={() => {
|
||||
|
|
@ -306,13 +290,12 @@ const AddPassThroughEndpoint: React.FC<AddFallbacksProps> = ({
|
|||
form.submit();
|
||||
}}
|
||||
>
|
||||
{isLoading ? 'Creating...' : 'Add Pass-Through Endpoint'}
|
||||
{isLoading ? "Creating..." : "Add Pass-Through Endpoint"}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -5,15 +5,7 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import { Typography } from "antd";
|
||||
import { useRouter } from "next/navigation";
|
||||
import {
|
||||
Button as Button2,
|
||||
Modal,
|
||||
Form,
|
||||
Input,
|
||||
Select as Select2,
|
||||
InputNumber,
|
||||
message,
|
||||
} from "antd";
|
||||
import { Button as Button2, Modal, Form, Input, Select as Select2, InputNumber, message } from "antd";
|
||||
import { CopyToClipboard } from "react-copy-to-clipboard";
|
||||
import { Select, SelectItem, Subtitle } from "@tremor/react";
|
||||
import { Team } from "./key_team_helpers/key_list";
|
||||
|
|
@ -42,7 +34,7 @@ import { PencilAltIcon } from "@heroicons/react/outline";
|
|||
import OnboardingModal from "./onboarding_link";
|
||||
import { InvitationLink } from "./onboarding_link";
|
||||
import SSOModals from "./SSOModals";
|
||||
import { ssoProviderConfigs } from './SSOModals';
|
||||
import { ssoProviderConfigs } from "./SSOModals";
|
||||
import SCIMConfig from "./SCIM";
|
||||
import UIAccessControlForm from "./UIAccessControlForm";
|
||||
import NotificationsManager from "./molecules/notifications_manager";
|
||||
|
|
@ -59,7 +51,6 @@ interface AdminPanelProps {
|
|||
}
|
||||
import { useBaseUrl } from "./constants";
|
||||
|
||||
|
||||
import {
|
||||
userUpdateUserCall,
|
||||
Member,
|
||||
|
|
@ -87,17 +78,13 @@ const AdminPanel: React.FC<AdminPanelProps> = ({
|
|||
const { Title, Paragraph } = Typography;
|
||||
const [value, setValue] = useState("");
|
||||
const [admins, setAdmins] = useState<null | any[]>(null);
|
||||
const [invitationLinkData, setInvitationLinkData] =
|
||||
useState<InvitationLink | null>(null);
|
||||
const [isInvitationLinkModalVisible, setIsInvitationLinkModalVisible] =
|
||||
useState(false);
|
||||
const [invitationLinkData, setInvitationLinkData] = useState<InvitationLink | null>(null);
|
||||
const [isInvitationLinkModalVisible, setIsInvitationLinkModalVisible] = useState(false);
|
||||
const [isAddMemberModalVisible, setIsAddMemberModalVisible] = useState(false);
|
||||
const [isAddAdminModalVisible, setIsAddAdminModalVisible] = useState(false);
|
||||
const [isUpdateMemberModalVisible, setIsUpdateModalModalVisible] =
|
||||
useState(false);
|
||||
const [isUpdateMemberModalVisible, setIsUpdateModalModalVisible] = useState(false);
|
||||
const [isAddSSOModalVisible, setIsAddSSOModalVisible] = useState(false);
|
||||
const [isInstructionsModalVisible, setIsInstructionsModalVisible] =
|
||||
useState(false);
|
||||
const [isInstructionsModalVisible, setIsInstructionsModalVisible] = useState(false);
|
||||
const [isAllowedIPModalVisible, setIsAllowedIPModalVisible] = useState(false);
|
||||
const [isAddIPModalVisible, setIsAddIPModalVisible] = useState(false);
|
||||
const [isDeleteIPModalVisible, setIsDeleteIPModalVisible] = useState(false);
|
||||
|
|
@ -107,14 +94,11 @@ const AdminPanel: React.FC<AdminPanelProps> = ({
|
|||
const [ssoConfigured, setSsoConfigured] = useState<boolean>(false);
|
||||
const router = useRouter();
|
||||
|
||||
const [possibleUIRoles, setPossibleUIRoles] = useState<null | Record<
|
||||
string,
|
||||
Record<string, string>
|
||||
>>(null);
|
||||
const [possibleUIRoles, setPossibleUIRoles] = useState<null | Record<string, Record<string, string>>>(null);
|
||||
|
||||
const isLocal = process.env.NODE_ENV === "development";
|
||||
if (isLocal != true) {
|
||||
console.log = function() {};
|
||||
console.log = function () {};
|
||||
}
|
||||
|
||||
const baseUrl = useBaseUrl();
|
||||
|
|
@ -129,13 +113,13 @@ const AdminPanel: React.FC<AdminPanelProps> = ({
|
|||
try {
|
||||
const ssoData = await getSSOSettings(accessToken);
|
||||
console.log("SSO data:", ssoData);
|
||||
|
||||
|
||||
// Check if any SSO provider is configured
|
||||
if (ssoData && ssoData.values) {
|
||||
const hasGoogleSSO = ssoData.values.google_client_id && ssoData.values.google_client_secret;
|
||||
const hasMicrosoftSSO = ssoData.values.microsoft_client_id && ssoData.values.microsoft_client_secret;
|
||||
const hasGenericSSO = ssoData.values.generic_client_id && ssoData.values.generic_client_secret;
|
||||
|
||||
|
||||
setSsoConfigured(hasGoogleSSO || hasMicrosoftSSO || hasGenericSSO);
|
||||
} else {
|
||||
setSsoConfigured(false);
|
||||
|
|
@ -151,9 +135,9 @@ const AdminPanel: React.FC<AdminPanelProps> = ({
|
|||
try {
|
||||
if (premiumUser !== true) {
|
||||
NotificationsManager.fromBackend(
|
||||
"This feature is only available for premium users. Please upgrade your account."
|
||||
)
|
||||
return
|
||||
"This feature is only available for premium users. Please upgrade your account.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (accessToken) {
|
||||
const data = await getAllowedIPs(accessToken);
|
||||
|
|
@ -171,7 +155,7 @@ const AdminPanel: React.FC<AdminPanelProps> = ({
|
|||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const handleAddIP = async (values: { ip: string }) => {
|
||||
try {
|
||||
if (accessToken) {
|
||||
|
|
@ -179,7 +163,7 @@ const AdminPanel: React.FC<AdminPanelProps> = ({
|
|||
// Fetch the updated list of IPs
|
||||
const updatedIPs = await getAllowedIPs(accessToken);
|
||||
setAllowedIPs(updatedIPs);
|
||||
NotificationsManager.success('IP address added successfully');
|
||||
NotificationsManager.success("IP address added successfully");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error adding IP:", error);
|
||||
|
|
@ -188,12 +172,12 @@ const AdminPanel: React.FC<AdminPanelProps> = ({
|
|||
setIsAddIPModalVisible(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const handleDeleteIP = async (ip: string) => {
|
||||
setIPToDelete(ip);
|
||||
setIsDeleteIPModalVisible(true);
|
||||
};
|
||||
|
||||
|
||||
const confirmDeleteIP = async () => {
|
||||
if (ipToDelete && accessToken) {
|
||||
try {
|
||||
|
|
@ -201,7 +185,7 @@ const AdminPanel: React.FC<AdminPanelProps> = ({
|
|||
// Fetch the updated list of IPs
|
||||
const updatedIPs = await getAllowedIPs(accessToken);
|
||||
setAllowedIPs(updatedIPs.length > 0 ? updatedIPs : [all_ip_address_allowed]);
|
||||
NotificationsManager.success('IP address deleted successfully');
|
||||
NotificationsManager.success("IP address deleted successfully");
|
||||
} catch (error) {
|
||||
console.error("Error deleting IP:", error);
|
||||
NotificationsManager.fromBackend(`Failed to delete IP address ${error}`);
|
||||
|
|
@ -212,7 +196,6 @@ const AdminPanel: React.FC<AdminPanelProps> = ({
|
|||
}
|
||||
};
|
||||
|
||||
|
||||
const handleAddSSOOk = () => {
|
||||
setIsAddSSOModalVisible(false);
|
||||
form.resetFields();
|
||||
|
|
@ -263,10 +246,7 @@ const AdminPanel: React.FC<AdminPanelProps> = ({
|
|||
const fetchProxyAdminInfo = async () => {
|
||||
if (accessToken != null) {
|
||||
const combinedList: any[] = [];
|
||||
const response = await userGetAllUsersCall(
|
||||
accessToken,
|
||||
"proxy_admin_viewer"
|
||||
);
|
||||
const response = await userGetAllUsersCall(accessToken, "proxy_admin_viewer");
|
||||
console.log("proxy admin viewer response: ", response);
|
||||
const proxyViewers: User[] = response["users"];
|
||||
console.log(`proxy viewers response: ${proxyViewers}`);
|
||||
|
|
@ -280,10 +260,7 @@ const AdminPanel: React.FC<AdminPanelProps> = ({
|
|||
|
||||
console.log(`proxy viewers: ${proxyViewers}`);
|
||||
|
||||
const response2 = await userGetAllUsersCall(
|
||||
accessToken,
|
||||
"proxy_admin"
|
||||
);
|
||||
const response2 = await userGetAllUsersCall(accessToken, "proxy_admin");
|
||||
|
||||
const proxyAdmins: User[] = response2["users"];
|
||||
|
||||
|
|
@ -362,10 +339,7 @@ const AdminPanel: React.FC<AdminPanelProps> = ({
|
|||
>
|
||||
<>
|
||||
<Form.Item label="Email" name="user_email" className="mb-8 mt-4">
|
||||
<Input
|
||||
name="user_email"
|
||||
className="px-3 py-2 border rounded-md w-full"
|
||||
/>
|
||||
<Input name="user_email" className="px-3 py-2 border rounded-md w-full" />
|
||||
</Form.Item>
|
||||
</>
|
||||
<div style={{ textAlign: "right", marginTop: "10px" }} className="mt-4">
|
||||
|
|
@ -375,11 +349,7 @@ const AdminPanel: React.FC<AdminPanelProps> = ({
|
|||
);
|
||||
};
|
||||
|
||||
const modifyMemberForm = (
|
||||
handleMemberUpdate: HandleMemberCreate,
|
||||
currentRole: string,
|
||||
userID: string
|
||||
) => {
|
||||
const modifyMemberForm = (handleMemberUpdate: HandleMemberCreate, currentRole: string, userID: string) => {
|
||||
return (
|
||||
<Form
|
||||
form={form}
|
||||
|
|
@ -426,17 +396,11 @@ const AdminPanel: React.FC<AdminPanelProps> = ({
|
|||
try {
|
||||
if (accessToken != null && admins != null) {
|
||||
NotificationsManager.info("Making API Call");
|
||||
const response: any = await userUpdateUserCall(
|
||||
accessToken,
|
||||
formValues,
|
||||
null
|
||||
);
|
||||
const response: any = await userUpdateUserCall(accessToken, formValues, null);
|
||||
console.log(`response for team create call: ${response}`);
|
||||
// Checking if the team exists in the list and updating or adding accordingly
|
||||
const foundIndex = admins.findIndex((user) => {
|
||||
console.log(
|
||||
`user.user_id=${user.user_id}; response.user_id=${response.user_id}`
|
||||
);
|
||||
console.log(`user.user_id=${user.user_id}; response.user_id=${response.user_id}`);
|
||||
return user.user_id === response.user_id;
|
||||
});
|
||||
console.log(`foundIndex: ${foundIndex}`);
|
||||
|
|
@ -458,11 +422,7 @@ const AdminPanel: React.FC<AdminPanelProps> = ({
|
|||
try {
|
||||
if (accessToken != null && admins != null) {
|
||||
NotificationsManager.info("Making API Call");
|
||||
const response: any = await userUpdateUserCall(
|
||||
accessToken,
|
||||
formValues,
|
||||
"proxy_admin_viewer"
|
||||
);
|
||||
const response: any = await userUpdateUserCall(accessToken, formValues, "proxy_admin_viewer");
|
||||
console.log(`response for team create call: ${response}`);
|
||||
// Checking if the team exists in the list and updating or adding accordingly
|
||||
|
||||
|
|
@ -474,9 +434,7 @@ const AdminPanel: React.FC<AdminPanelProps> = ({
|
|||
});
|
||||
|
||||
const foundIndex = admins.findIndex((user) => {
|
||||
console.log(
|
||||
`user.user_id=${user.user_id}; response.user_id=${response.user_id}`
|
||||
);
|
||||
console.log(`user.user_id=${user.user_id}; response.user_id=${response.user_id}`);
|
||||
return user.user_id === response.user_id;
|
||||
});
|
||||
console.log(`foundIndex: ${foundIndex}`);
|
||||
|
|
@ -502,11 +460,7 @@ const AdminPanel: React.FC<AdminPanelProps> = ({
|
|||
user_email: formValues.user_email,
|
||||
user_id: formValues.user_id,
|
||||
};
|
||||
const response: any = await userUpdateUserCall(
|
||||
accessToken,
|
||||
formValues,
|
||||
"proxy_admin"
|
||||
);
|
||||
const response: any = await userUpdateUserCall(accessToken, formValues, "proxy_admin");
|
||||
|
||||
// Give admin an invite link for inviting user to proxy
|
||||
const user_id = response.data?.user_id || response.user_id;
|
||||
|
|
@ -517,9 +471,7 @@ const AdminPanel: React.FC<AdminPanelProps> = ({
|
|||
console.log(`response for team create call: ${response}`);
|
||||
// Checking if the team exists in the list and updating or adding accordingly
|
||||
const foundIndex = admins.findIndex((user) => {
|
||||
console.log(
|
||||
`user.user_id=${user.user_id}; response.user_id=${user_id}`
|
||||
);
|
||||
console.log(`user.user_id=${user.user_id}; response.user_id=${user_id}`);
|
||||
return user.user_id === response.user_id;
|
||||
});
|
||||
console.log(`foundIndex: ${foundIndex}`);
|
||||
|
|
@ -559,34 +511,47 @@ const AdminPanel: React.FC<AdminPanelProps> = ({
|
|||
<TabPanel>
|
||||
<Card>
|
||||
<Title level={4}> ✨ Security Settings</Title>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem', marginTop: '1rem', marginLeft: '0.5rem' }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "1rem",
|
||||
marginTop: "1rem",
|
||||
marginLeft: "0.5rem",
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<Button
|
||||
style={{ width: '150px' }}
|
||||
onClick={() => premiumUser === true ? setIsAddSSOModalVisible(true) : NotificationsManager.fromBackend("Only premium users can add SSO")}
|
||||
<Button
|
||||
style={{ width: "150px" }}
|
||||
onClick={() =>
|
||||
premiumUser === true
|
||||
? setIsAddSSOModalVisible(true)
|
||||
: NotificationsManager.fromBackend("Only premium users can add SSO")
|
||||
}
|
||||
>
|
||||
{ssoConfigured ? "Edit SSO Settings" : "Add SSO"}
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
style={{ width: '150px' }}
|
||||
onClick={handleShowAllowedIPs}
|
||||
>
|
||||
<Button style={{ width: "150px" }} onClick={handleShowAllowedIPs}>
|
||||
Allowed IPs
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
style={{ width: '150px' }}
|
||||
onClick={() => premiumUser === true ? setIsUIAccessControlModalVisible(true) : NotificationsManager.fromBackend("Only premium users can configure UI access control")}
|
||||
<Button
|
||||
style={{ width: "150px" }}
|
||||
onClick={() =>
|
||||
premiumUser === true
|
||||
? setIsUIAccessControlModalVisible(true)
|
||||
: NotificationsManager.fromBackend("Only premium users can configure UI access control")
|
||||
}
|
||||
>
|
||||
UI Access Control
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
|
||||
<div className="flex justify-start mb-4">
|
||||
<SSOModals
|
||||
isAddSSOModalVisible={isAddSSOModalVisible}
|
||||
|
|
@ -601,112 +566,103 @@ const AdminPanel: React.FC<AdminPanelProps> = ({
|
|||
ssoConfigured={ssoConfigured}
|
||||
/>
|
||||
<Modal
|
||||
title="Manage Allowed IP Addresses"
|
||||
width={800}
|
||||
visible={isAllowedIPModalVisible}
|
||||
onCancel={() => setIsAllowedIPModalVisible(false)}
|
||||
footer={[
|
||||
<Button className="mx-1"key="add" onClick={() => setIsAddIPModalVisible(true)}>
|
||||
Add IP Address
|
||||
</Button>,
|
||||
<Button key="close" onClick={() => setIsAllowedIPModalVisible(false)}>
|
||||
Close
|
||||
</Button>
|
||||
]}
|
||||
>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableHeaderCell>IP Address</TableHeaderCell>
|
||||
<TableHeaderCell className="text-right">Action</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{allowedIPs.map((ip, index) => (
|
||||
<TableRow key={index}>
|
||||
<TableCell>{ip}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{ip !== all_ip_address_allowed && (
|
||||
<Button onClick={() => handleDeleteIP(ip)} color="red" size="xs">
|
||||
Delete
|
||||
</Button>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Modal>
|
||||
title="Manage Allowed IP Addresses"
|
||||
width={800}
|
||||
visible={isAllowedIPModalVisible}
|
||||
onCancel={() => setIsAllowedIPModalVisible(false)}
|
||||
footer={[
|
||||
<Button className="mx-1" key="add" onClick={() => setIsAddIPModalVisible(true)}>
|
||||
Add IP Address
|
||||
</Button>,
|
||||
<Button key="close" onClick={() => setIsAllowedIPModalVisible(false)}>
|
||||
Close
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableHeaderCell>IP Address</TableHeaderCell>
|
||||
<TableHeaderCell className="text-right">Action</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{allowedIPs.map((ip, index) => (
|
||||
<TableRow key={index}>
|
||||
<TableCell>{ip}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{ip !== all_ip_address_allowed && (
|
||||
<Button onClick={() => handleDeleteIP(ip)} color="red" size="xs">
|
||||
Delete
|
||||
</Button>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title="Add Allowed IP Address"
|
||||
visible={isAddIPModalVisible}
|
||||
onCancel={() => setIsAddIPModalVisible(false)}
|
||||
footer={null}
|
||||
>
|
||||
<Form onFinish={handleAddIP}>
|
||||
<Form.Item
|
||||
name="ip"
|
||||
rules={[{ required: true, message: 'Please enter an IP address' }]}
|
||||
>
|
||||
<Input placeholder="Enter IP address" />
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button2 htmlType="submit">
|
||||
Add IP Address
|
||||
</Button2>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
<Modal
|
||||
title="Add Allowed IP Address"
|
||||
visible={isAddIPModalVisible}
|
||||
onCancel={() => setIsAddIPModalVisible(false)}
|
||||
footer={null}
|
||||
>
|
||||
<Form onFinish={handleAddIP}>
|
||||
<Form.Item name="ip" rules={[{ required: true, message: "Please enter an IP address" }]}>
|
||||
<Input placeholder="Enter IP address" />
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button2 htmlType="submit">Add IP Address</Button2>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title="Confirm Delete"
|
||||
visible={isDeleteIPModalVisible}
|
||||
onCancel={() => setIsDeleteIPModalVisible(false)}
|
||||
onOk={confirmDeleteIP}
|
||||
footer={[
|
||||
<Button className="mx-1"key="delete" onClick={() => confirmDeleteIP()}>
|
||||
Yes
|
||||
</Button>,
|
||||
<Button key="close" onClick={() => setIsDeleteIPModalVisible(false)}>
|
||||
Close
|
||||
</Button>
|
||||
]}
|
||||
>
|
||||
<p>Are you sure you want to delete the IP address: {ipToDelete}?</p>
|
||||
</Modal>
|
||||
<Modal
|
||||
title="Confirm Delete"
|
||||
visible={isDeleteIPModalVisible}
|
||||
onCancel={() => setIsDeleteIPModalVisible(false)}
|
||||
onOk={confirmDeleteIP}
|
||||
footer={[
|
||||
<Button className="mx-1" key="delete" onClick={() => confirmDeleteIP()}>
|
||||
Yes
|
||||
</Button>,
|
||||
<Button key="close" onClick={() => setIsDeleteIPModalVisible(false)}>
|
||||
Close
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<p>Are you sure you want to delete the IP address: {ipToDelete}?</p>
|
||||
</Modal>
|
||||
|
||||
{/* UI Access Control Modal */}
|
||||
<Modal
|
||||
title="UI Access Control Settings"
|
||||
visible={isUIAccessControlModalVisible}
|
||||
width={600}
|
||||
footer={null}
|
||||
onOk={handleUIAccessControlOk}
|
||||
onCancel={handleUIAccessControlCancel}
|
||||
>
|
||||
<UIAccessControlForm
|
||||
accessToken={accessToken}
|
||||
onSuccess={() => {
|
||||
handleUIAccessControlOk();
|
||||
NotificationsManager.success("UI Access Control settings updated successfully");
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
</div>
|
||||
<Callout title="Login without SSO" color="teal">
|
||||
If you need to login without sso, you can access{" "}
|
||||
<a href={nonSssoUrl} target="_blank">
|
||||
<b>{nonSssoUrl}</b>{" "}
|
||||
</a>
|
||||
</Callout>
|
||||
{/* UI Access Control Modal */}
|
||||
<Modal
|
||||
title="UI Access Control Settings"
|
||||
visible={isUIAccessControlModalVisible}
|
||||
width={600}
|
||||
footer={null}
|
||||
onOk={handleUIAccessControlOk}
|
||||
onCancel={handleUIAccessControlCancel}
|
||||
>
|
||||
<UIAccessControlForm
|
||||
accessToken={accessToken}
|
||||
onSuccess={() => {
|
||||
handleUIAccessControlOk();
|
||||
NotificationsManager.success("UI Access Control settings updated successfully");
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
</div>
|
||||
<Callout title="Login without SSO" color="teal">
|
||||
If you need to login without sso, you can access{" "}
|
||||
<a href={nonSssoUrl} target="_blank">
|
||||
<b>{nonSssoUrl}</b>{" "}
|
||||
</a>
|
||||
</Callout>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<SCIMConfig
|
||||
accessToken={accessToken}
|
||||
userID={userID}
|
||||
proxySettings={proxySettings}
|
||||
/>
|
||||
<SCIMConfig accessToken={accessToken} userID={userID} proxySettings={proxySettings} />
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
|
|
|
|||
|
|
@ -34,13 +34,8 @@ interface AlertingSettingsProps {
|
|||
premiumUser: boolean;
|
||||
}
|
||||
|
||||
const AlertingSettings: React.FC<AlertingSettingsProps> = ({
|
||||
accessToken,
|
||||
premiumUser,
|
||||
}) => {
|
||||
const [alertingSettings, setAlertingSettings] = useState<
|
||||
alertingSettingsItem[]
|
||||
>([]);
|
||||
const AlertingSettings: React.FC<AlertingSettingsProps> = ({ accessToken, premiumUser }) => {
|
||||
const [alertingSettings, setAlertingSettings] = useState<alertingSettingsItem[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
// get values
|
||||
|
|
@ -55,12 +50,10 @@ const AlertingSettings: React.FC<AlertingSettingsProps> = ({
|
|||
const handleInputChange = (fieldName: string, newValue: any) => {
|
||||
// Update the value in the state
|
||||
const updatedSettings = alertingSettings.map((setting) =>
|
||||
setting.field_name === fieldName
|
||||
? { ...setting, field_value: newValue }
|
||||
: setting
|
||||
setting.field_name === fieldName ? { ...setting, field_value: newValue } : setting,
|
||||
);
|
||||
|
||||
console.log(`updatedSettings: ${JSON.stringify(updatedSettings)}`)
|
||||
console.log(`updatedSettings: ${JSON.stringify(updatedSettings)}`);
|
||||
setAlertingSettings(updatedSettings);
|
||||
};
|
||||
|
||||
|
|
@ -69,7 +62,7 @@ const AlertingSettings: React.FC<AlertingSettingsProps> = ({
|
|||
return;
|
||||
}
|
||||
|
||||
console.log(`formValues: ${formValues}`)
|
||||
console.log(`formValues: ${formValues}`);
|
||||
let fieldValue = formValues;
|
||||
|
||||
if (fieldValue == null || fieldValue == undefined) {
|
||||
|
|
@ -77,16 +70,16 @@ const AlertingSettings: React.FC<AlertingSettingsProps> = ({
|
|||
}
|
||||
|
||||
const initialFormValues: Record<string, any> = {};
|
||||
|
||||
|
||||
alertingSettings.forEach((setting) => {
|
||||
initialFormValues[setting.field_name] = setting.field_value;
|
||||
});
|
||||
|
||||
// Merge initialFormValues with actual formValues
|
||||
const mergedFormValues = { ...formValues, ...initialFormValues };
|
||||
console.log(`mergedFormValues: ${JSON.stringify(mergedFormValues)}`)
|
||||
console.log(`mergedFormValues: ${JSON.stringify(mergedFormValues)}`);
|
||||
const { slack_alerting, ...alertingArgs } = mergedFormValues;
|
||||
console.log(`slack_alerting: ${slack_alerting}, alertingArgs: ${JSON.stringify(alertingArgs)}`)
|
||||
console.log(`slack_alerting: ${slack_alerting}, alertingArgs: ${JSON.stringify(alertingArgs)}`);
|
||||
try {
|
||||
updateConfigFieldSetting(accessToken, "alerting_args", alertingArgs);
|
||||
if (typeof slack_alerting === "boolean") {
|
||||
|
|
@ -119,7 +112,7 @@ const AlertingSettings: React.FC<AlertingSettingsProps> = ({
|
|||
stored_in_db: null,
|
||||
field_value: setting.field_default_value,
|
||||
}
|
||||
: setting
|
||||
: setting,
|
||||
);
|
||||
setAlertingSettings(updatedSettings);
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -30,15 +30,15 @@ const DynamicForm: React.FC<DynamicFormProps> = ({
|
|||
const [form] = Form.useForm();
|
||||
|
||||
const onFinish = () => {
|
||||
console.log(`INSIDE ONFINISH`)
|
||||
console.log(`INSIDE ONFINISH`);
|
||||
const formData = form.getFieldsValue();
|
||||
const isEmpty = Object.entries(formData).every(([key, value]) => {
|
||||
if (typeof value === 'boolean') {
|
||||
if (typeof value === "boolean") {
|
||||
return false; // Boolean values are never considered empty
|
||||
}
|
||||
return value === '' || value === null || value === undefined;
|
||||
return value === "" || value === null || value === undefined;
|
||||
});
|
||||
console.log(`formData: ${JSON.stringify(formData)}, isEmpty: ${isEmpty}`)
|
||||
console.log(`formData: ${JSON.stringify(formData)}, isEmpty: ${isEmpty}`);
|
||||
if (!isEmpty) {
|
||||
handleSubmit(formData);
|
||||
} else {
|
||||
|
|
@ -79,10 +79,7 @@ const DynamicForm: React.FC<DynamicFormProps> = ({
|
|||
onChange={(checked) => handleInputChange(value.field_name, checked)}
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
value={value.field_value}
|
||||
onChange={(e) => handleInputChange(value.field_name, e)}
|
||||
/>
|
||||
<Input value={value.field_value} onChange={(e) => handleInputChange(value.field_name, e)} />
|
||||
)}
|
||||
</TableCell>
|
||||
</Form.Item>
|
||||
|
|
@ -96,7 +93,11 @@ const DynamicForm: React.FC<DynamicFormProps> = ({
|
|||
</TableCell>
|
||||
)
|
||||
) : (
|
||||
<Form.Item name={value.field_name} className="mb-0" valuePropName={value.field_type === "Boolean" ? "checked" : "value"}>
|
||||
<Form.Item
|
||||
name={value.field_name}
|
||||
className="mb-0"
|
||||
valuePropName={value.field_type === "Boolean" ? "checked" : "value"}
|
||||
>
|
||||
<TableCell>
|
||||
{value.field_type === "Integer" ? (
|
||||
<InputNumber
|
||||
|
|
@ -112,14 +113,9 @@ const DynamicForm: React.FC<DynamicFormProps> = ({
|
|||
handleInputChange(value.field_name, checked);
|
||||
form.setFieldsValue({ [value.field_name]: checked });
|
||||
}}
|
||||
|
||||
|
||||
/>
|
||||
) :(
|
||||
<Input
|
||||
value={value.field_value}
|
||||
onChange={(e) => handleInputChange(value.field_name, e)}
|
||||
/>
|
||||
) : (
|
||||
<Input value={value.field_value} onChange={(e) => handleInputChange(value.field_name, e)} />
|
||||
)}
|
||||
</TableCell>
|
||||
</Form.Item>
|
||||
|
|
@ -136,11 +132,7 @@ const DynamicForm: React.FC<DynamicFormProps> = ({
|
|||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Icon
|
||||
icon={TrashIcon}
|
||||
color="red"
|
||||
onClick={() => handleResetField(value.field_name, index)}
|
||||
>
|
||||
<Icon icon={TrashIcon} color="red" onClick={() => handleResetField(value.field_name, index)}>
|
||||
Reset
|
||||
</Icon>
|
||||
</TableCell>
|
||||
|
|
|
|||
|
|
@ -1,68 +1,68 @@
|
|||
"use client"
|
||||
import React, { useEffect, useState, useCallback, useRef } from "react"
|
||||
import { ColumnDef, Row } from "@tanstack/react-table"
|
||||
import { DataTable } from "./view_logs/table"
|
||||
import { Select, SelectItem } from "@tremor/react"
|
||||
import { Button } from "@tremor/react"
|
||||
import KeyInfoView from "./templates/key_info_view"
|
||||
import { Tooltip } from "antd"
|
||||
import { Team, KeyResponse } from "./key_team_helpers/key_list"
|
||||
import FilterComponent from "./molecules/filter"
|
||||
import { FilterOption } from "./molecules/filter"
|
||||
import { keyListCall, Organization, userListCall } from "./networking"
|
||||
import { createTeamSearchFunction } from "./key_team_helpers/team_search_fn"
|
||||
import { createOrgSearchFunction } from "./key_team_helpers/organization_search_fn"
|
||||
import { useFilterLogic } from "./key_team_helpers/filter_logic"
|
||||
import { Setter } from "@/types"
|
||||
import { updateExistingKeys } from "@/utils/dataUtils"
|
||||
import { debounce } from "lodash"
|
||||
import { defaultPageSize } from "./constants"
|
||||
import { fetchAllTeams } from "./key_team_helpers/filter_helpers"
|
||||
import { fetchAllOrganizations } from "./key_team_helpers/filter_helpers"
|
||||
import { flexRender, getCoreRowModel, getSortedRowModel, SortingState, useReactTable } from "@tanstack/react-table"
|
||||
import { Table, TableHead, TableHeaderCell, TableBody, TableRow, TableCell, Icon } from "@tremor/react"
|
||||
import { SwitchVerticalIcon, ChevronUpIcon, ChevronDownIcon, ChevronRightIcon } from "@heroicons/react/outline"
|
||||
import { Badge, Text } from "@tremor/react"
|
||||
import { getModelDisplayName } from "./key_team_helpers/fetch_available_models_team_key"
|
||||
import { formatNumberWithCommas } from "@/utils/dataUtils"
|
||||
"use client";
|
||||
import React, { useEffect, useState, useCallback, useRef } from "react";
|
||||
import { ColumnDef, Row } from "@tanstack/react-table";
|
||||
import { DataTable } from "./view_logs/table";
|
||||
import { Select, SelectItem } from "@tremor/react";
|
||||
import { Button } from "@tremor/react";
|
||||
import KeyInfoView from "./templates/key_info_view";
|
||||
import { Tooltip } from "antd";
|
||||
import { Team, KeyResponse } from "./key_team_helpers/key_list";
|
||||
import FilterComponent from "./molecules/filter";
|
||||
import { FilterOption } from "./molecules/filter";
|
||||
import { keyListCall, Organization, userListCall } from "./networking";
|
||||
import { createTeamSearchFunction } from "./key_team_helpers/team_search_fn";
|
||||
import { createOrgSearchFunction } from "./key_team_helpers/organization_search_fn";
|
||||
import { useFilterLogic } from "./key_team_helpers/filter_logic";
|
||||
import { Setter } from "@/types";
|
||||
import { updateExistingKeys } from "@/utils/dataUtils";
|
||||
import { debounce } from "lodash";
|
||||
import { defaultPageSize } from "./constants";
|
||||
import { fetchAllTeams } from "./key_team_helpers/filter_helpers";
|
||||
import { fetchAllOrganizations } from "./key_team_helpers/filter_helpers";
|
||||
import { flexRender, getCoreRowModel, getSortedRowModel, SortingState, useReactTable } from "@tanstack/react-table";
|
||||
import { Table, TableHead, TableHeaderCell, TableBody, TableRow, TableCell, Icon } from "@tremor/react";
|
||||
import { SwitchVerticalIcon, ChevronUpIcon, ChevronDownIcon, ChevronRightIcon } from "@heroicons/react/outline";
|
||||
import { Badge, Text } from "@tremor/react";
|
||||
import { getModelDisplayName } from "./key_team_helpers/fetch_available_models_team_key";
|
||||
import { formatNumberWithCommas } from "@/utils/dataUtils";
|
||||
|
||||
interface AllKeysTableProps {
|
||||
keys: KeyResponse[]
|
||||
setKeys: (keys: KeyResponse[] | ((prev: KeyResponse[]) => KeyResponse[])) => void
|
||||
isLoading?: boolean
|
||||
keys: KeyResponse[];
|
||||
setKeys: (keys: KeyResponse[] | ((prev: KeyResponse[]) => KeyResponse[])) => void;
|
||||
isLoading?: boolean;
|
||||
pagination: {
|
||||
currentPage: number
|
||||
totalPages: number
|
||||
totalCount: number
|
||||
}
|
||||
onPageChange: (page: number) => void
|
||||
pageSize?: number
|
||||
teams: Team[] | null
|
||||
selectedTeam: Team | null
|
||||
setSelectedTeam: (team: Team | null) => void
|
||||
selectedKeyAlias: string | null
|
||||
setSelectedKeyAlias: Setter<string | null>
|
||||
accessToken: string | null
|
||||
userID: string | null
|
||||
userRole: string | null
|
||||
organizations: Organization[] | null
|
||||
setCurrentOrg: React.Dispatch<React.SetStateAction<Organization | null>>
|
||||
refresh?: () => void
|
||||
onSortChange?: (sortBy: string, sortOrder: "asc" | "desc") => void
|
||||
currentPage: number;
|
||||
totalPages: number;
|
||||
totalCount: number;
|
||||
};
|
||||
onPageChange: (page: number) => void;
|
||||
pageSize?: number;
|
||||
teams: Team[] | null;
|
||||
selectedTeam: Team | null;
|
||||
setSelectedTeam: (team: Team | null) => void;
|
||||
selectedKeyAlias: string | null;
|
||||
setSelectedKeyAlias: Setter<string | null>;
|
||||
accessToken: string | null;
|
||||
userID: string | null;
|
||||
userRole: string | null;
|
||||
organizations: Organization[] | null;
|
||||
setCurrentOrg: React.Dispatch<React.SetStateAction<Organization | null>>;
|
||||
refresh?: () => void;
|
||||
onSortChange?: (sortBy: string, sortOrder: "asc" | "desc") => void;
|
||||
currentSort?: {
|
||||
sortBy: string
|
||||
sortOrder: "asc" | "desc"
|
||||
}
|
||||
premiumUser: boolean
|
||||
setAccessToken?: (token: string) => void
|
||||
sortBy: string;
|
||||
sortOrder: "asc" | "desc";
|
||||
};
|
||||
premiumUser: boolean;
|
||||
setAccessToken?: (token: string) => void;
|
||||
}
|
||||
|
||||
// Define columns similar to our logs table
|
||||
|
||||
interface UserResponse {
|
||||
user_id: string
|
||||
user_email: string
|
||||
user_role: string
|
||||
user_id: string;
|
||||
user_email: string;
|
||||
user_role: string;
|
||||
}
|
||||
|
||||
const TeamFilter = ({
|
||||
|
|
@ -70,14 +70,14 @@ const TeamFilter = ({
|
|||
selectedTeam,
|
||||
setSelectedTeam,
|
||||
}: {
|
||||
teams: Team[] | null
|
||||
selectedTeam: Team | null
|
||||
setSelectedTeam: (team: Team | null) => void
|
||||
teams: Team[] | null;
|
||||
selectedTeam: Team | null;
|
||||
setSelectedTeam: (team: Team | null) => void;
|
||||
}) => {
|
||||
const handleTeamChange = (value: string) => {
|
||||
const team = teams?.find((t) => t.team_id === value)
|
||||
setSelectedTeam(team || null)
|
||||
}
|
||||
const team = teams?.find((t) => t.team_id === value);
|
||||
setSelectedTeam(team || null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-4">
|
||||
|
|
@ -99,8 +99,8 @@ const TeamFilter = ({
|
|||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* AllKeysTable – a new table for keys that mimics the table styling used in view_logs.
|
||||
|
|
@ -130,8 +130,8 @@ export function AllKeysTable({
|
|||
premiumUser,
|
||||
setAccessToken,
|
||||
}: AllKeysTableProps) {
|
||||
const [selectedKeyId, setSelectedKeyId] = useState<string | null>(null)
|
||||
const [userList, setUserList] = useState<UserResponse[]>([])
|
||||
const [selectedKeyId, setSelectedKeyId] = useState<string | null>(null);
|
||||
const [userList, setUserList] = useState<UserResponse[]>([]);
|
||||
const [sorting, setSorting] = React.useState<SortingState>(() => {
|
||||
if (currentSort) {
|
||||
return [
|
||||
|
|
@ -139,16 +139,16 @@ export function AllKeysTable({
|
|||
id: currentSort.sortBy,
|
||||
desc: currentSort.sortOrder === "desc",
|
||||
},
|
||||
]
|
||||
];
|
||||
}
|
||||
return [
|
||||
{
|
||||
id: "created_at",
|
||||
desc: true,
|
||||
},
|
||||
]
|
||||
})
|
||||
const [expandedAccordions, setExpandedAccordions] = useState<Record<string, boolean>>({})
|
||||
];
|
||||
});
|
||||
const [expandedAccordions, setExpandedAccordions] = useState<Record<string, boolean>>({});
|
||||
|
||||
// Use the filter logic hook
|
||||
|
||||
|
|
@ -158,34 +158,34 @@ export function AllKeysTable({
|
|||
teams,
|
||||
organizations,
|
||||
accessToken,
|
||||
})
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (accessToken) {
|
||||
const user_IDs = keys.map((key) => key.user_id).filter((id) => id !== null)
|
||||
const user_IDs = keys.map((key) => key.user_id).filter((id) => id !== null);
|
||||
const fetchUserList = async () => {
|
||||
const userListData = await userListCall(accessToken, user_IDs, 1, 100)
|
||||
setUserList(userListData.users)
|
||||
}
|
||||
fetchUserList()
|
||||
const userListData = await userListCall(accessToken, user_IDs, 1, 100);
|
||||
setUserList(userListData.users);
|
||||
};
|
||||
fetchUserList();
|
||||
}
|
||||
}, [accessToken, keys])
|
||||
}, [accessToken, keys]);
|
||||
|
||||
// Add a useEffect to call refresh when a key is created
|
||||
useEffect(() => {
|
||||
if (refresh) {
|
||||
const handleStorageChange = () => {
|
||||
refresh()
|
||||
}
|
||||
refresh();
|
||||
};
|
||||
|
||||
// Listen for storage events that might indicate a key was created
|
||||
window.addEventListener("storage", handleStorageChange)
|
||||
window.addEventListener("storage", handleStorageChange);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("storage", handleStorageChange)
|
||||
}
|
||||
window.removeEventListener("storage", handleStorageChange);
|
||||
};
|
||||
}
|
||||
}, [refresh])
|
||||
}, [refresh]);
|
||||
|
||||
const columns: ColumnDef<KeyResponse>[] = [
|
||||
{
|
||||
|
|
@ -222,8 +222,10 @@ export function AllKeysTable({
|
|||
accessorKey: "key_alias",
|
||||
header: "Key Alias",
|
||||
cell: (info) => {
|
||||
const value = info.getValue() as string
|
||||
return <Tooltip title={value}>{value ? (value.length > 20 ? `${value.slice(0, 20)}...` : value) : "-"}</Tooltip>
|
||||
const value = info.getValue() as string;
|
||||
return (
|
||||
<Tooltip title={value}>{value ? (value.length > 20 ? `${value.slice(0, 20)}...` : value) : "-"}</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -237,9 +239,9 @@ export function AllKeysTable({
|
|||
accessorKey: "team_id",
|
||||
header: "Team Alias",
|
||||
cell: ({ row, getValue }) => {
|
||||
const teamId = getValue() as string
|
||||
const team = teams?.find((t) => t.team_id === teamId)
|
||||
return team?.team_alias || "Unknown"
|
||||
const teamId = getValue() as string;
|
||||
const team = teams?.find((t) => t.team_id === teamId);
|
||||
return team?.team_alias || "Unknown";
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -263,15 +265,15 @@ export function AllKeysTable({
|
|||
accessorKey: "user_id",
|
||||
header: "User Email",
|
||||
cell: (info) => {
|
||||
const userId = info.getValue() as string
|
||||
const user = userList.find((u) => u.user_id === userId)
|
||||
const userId = info.getValue() as string;
|
||||
const user = userList.find((u) => u.user_id === userId);
|
||||
return user?.user_email ? (
|
||||
<Tooltip title={user?.user_email}>
|
||||
<span>{user?.user_email.slice(0, 20)}...</span>
|
||||
</Tooltip>
|
||||
) : (
|
||||
"-"
|
||||
)
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -279,15 +281,15 @@ export function AllKeysTable({
|
|||
accessorKey: "user_id",
|
||||
header: "User ID",
|
||||
cell: (info) => {
|
||||
const userId = info.getValue() as string | null
|
||||
const userId = info.getValue() as string | null;
|
||||
if (userId && userId.length > 15) {
|
||||
return (
|
||||
<Tooltip title={userId}>
|
||||
<span>{userId.slice(0, 7)}...</span>
|
||||
</Tooltip>
|
||||
)
|
||||
);
|
||||
}
|
||||
return userId ? userId : "-"
|
||||
return userId ? userId : "-";
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -295,8 +297,8 @@ export function AllKeysTable({
|
|||
accessorKey: "created_at",
|
||||
header: "Created At",
|
||||
cell: (info) => {
|
||||
const value = info.getValue()
|
||||
return value ? new Date(value as string).toLocaleDateString() : "-"
|
||||
const value = info.getValue();
|
||||
return value ? new Date(value as string).toLocaleDateString() : "-";
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -304,15 +306,15 @@ export function AllKeysTable({
|
|||
accessorKey: "created_by",
|
||||
header: "Created By",
|
||||
cell: (info) => {
|
||||
const value = info.getValue() as string | null
|
||||
const value = info.getValue() as string | null;
|
||||
if (value && value.length > 15) {
|
||||
return (
|
||||
<Tooltip title={value}>
|
||||
<span>{value.slice(0, 7)}...</span>
|
||||
</Tooltip>
|
||||
)
|
||||
);
|
||||
}
|
||||
return value
|
||||
return value;
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -320,8 +322,8 @@ export function AllKeysTable({
|
|||
accessorKey: "updated_at",
|
||||
header: "Updated At",
|
||||
cell: (info) => {
|
||||
const value = info.getValue()
|
||||
return value ? new Date(value as string).toLocaleDateString() : "Never"
|
||||
const value = info.getValue();
|
||||
return value ? new Date(value as string).toLocaleDateString() : "Never";
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -329,8 +331,8 @@ export function AllKeysTable({
|
|||
accessorKey: "expires",
|
||||
header: "Expires",
|
||||
cell: (info) => {
|
||||
const value = info.getValue()
|
||||
return value ? new Date(value as string).toLocaleDateString() : "Never"
|
||||
const value = info.getValue();
|
||||
return value ? new Date(value as string).toLocaleDateString() : "Never";
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -344,11 +346,11 @@ export function AllKeysTable({
|
|||
accessorKey: "max_budget",
|
||||
header: "Budget (USD)",
|
||||
cell: (info) => {
|
||||
const maxBudget = info.getValue() as number | null
|
||||
const maxBudget = info.getValue() as number | null;
|
||||
if (maxBudget === null) {
|
||||
return "Unlimited"
|
||||
return "Unlimited";
|
||||
}
|
||||
return `$${formatNumberWithCommas(maxBudget)}`
|
||||
return `$${formatNumberWithCommas(maxBudget)}`;
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -356,8 +358,8 @@ export function AllKeysTable({
|
|||
accessorKey: "budget_reset_at",
|
||||
header: "Budget Reset",
|
||||
cell: (info) => {
|
||||
const value = info.getValue()
|
||||
return value ? new Date(value as string).toLocaleString() : "Never"
|
||||
const value = info.getValue();
|
||||
return value ? new Date(value as string).toLocaleString() : "Never";
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -365,7 +367,7 @@ export function AllKeysTable({
|
|||
accessorKey: "models",
|
||||
header: "Models",
|
||||
cell: (info) => {
|
||||
const models = info.getValue() as string[]
|
||||
const models = info.getValue() as string[];
|
||||
return (
|
||||
<div className="flex flex-col py-2">
|
||||
{Array.isArray(models) ? (
|
||||
|
|
@ -387,7 +389,7 @@ export function AllKeysTable({
|
|||
setExpandedAccordions((prev) => ({
|
||||
...prev,
|
||||
[info.row.id]: !prev[info.row.id],
|
||||
}))
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -441,23 +443,23 @@ export function AllKeysTable({
|
|||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "rate_limits",
|
||||
header: "Rate Limits",
|
||||
cell: ({ row }) => {
|
||||
const key = row.original
|
||||
const key = row.original;
|
||||
return (
|
||||
<div>
|
||||
<div>TPM: {key.tpm_limit !== null ? key.tpm_limit : "Unlimited"}</div>
|
||||
<div>RPM: {key.rpm_limit !== null ? key.rpm_limit : "Unlimited"}</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
},
|
||||
},
|
||||
]
|
||||
];
|
||||
|
||||
const filterOptions: FilterOption[] = [
|
||||
{
|
||||
|
|
@ -465,18 +467,18 @@ export function AllKeysTable({
|
|||
label: "Team ID",
|
||||
isSearchable: true,
|
||||
searchFn: async (searchText: string) => {
|
||||
if (!allTeams || allTeams.length === 0) return []
|
||||
if (!allTeams || allTeams.length === 0) return [];
|
||||
|
||||
const filteredTeams = allTeams.filter(
|
||||
(team) =>
|
||||
team.team_id.toLowerCase().includes(searchText.toLowerCase()) ||
|
||||
(team.team_alias && team.team_alias.toLowerCase().includes(searchText.toLowerCase())),
|
||||
)
|
||||
);
|
||||
|
||||
return filteredTeams.map((team) => ({
|
||||
label: `${team.team_alias || team.team_id} (${team.team_id})`,
|
||||
value: team.team_id,
|
||||
}))
|
||||
}));
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -484,18 +486,18 @@ export function AllKeysTable({
|
|||
label: "Organization ID",
|
||||
isSearchable: true,
|
||||
searchFn: async (searchText: string) => {
|
||||
if (!allOrganizations || allOrganizations.length === 0) return []
|
||||
if (!allOrganizations || allOrganizations.length === 0) return [];
|
||||
|
||||
const filteredOrgs = allOrganizations.filter(
|
||||
(org) => org.organization_id?.toLowerCase().includes(searchText.toLowerCase()) ?? false,
|
||||
)
|
||||
);
|
||||
|
||||
return filteredOrgs
|
||||
.filter((org) => org.organization_id !== null && org.organization_id !== undefined)
|
||||
.map((org) => ({
|
||||
label: `${org.organization_id || "Unknown"} (${org.organization_id})`,
|
||||
value: org.organization_id as string,
|
||||
}))
|
||||
}));
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -504,15 +506,15 @@ export function AllKeysTable({
|
|||
isSearchable: true,
|
||||
searchFn: async (searchText) => {
|
||||
const filteredKeyAliases = allKeyAliases.filter((key) => {
|
||||
return key.toLowerCase().includes(searchText.toLowerCase())
|
||||
})
|
||||
return key.toLowerCase().includes(searchText.toLowerCase());
|
||||
});
|
||||
|
||||
return filteredKeyAliases.map((key) => {
|
||||
return {
|
||||
label: key,
|
||||
value: key,
|
||||
}
|
||||
})
|
||||
};
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -525,9 +527,9 @@ export function AllKeysTable({
|
|||
label: "Key Hash",
|
||||
isSearchable: false,
|
||||
},
|
||||
]
|
||||
];
|
||||
|
||||
console.log(`keys: ${JSON.stringify(keys)}`)
|
||||
console.log(`keys: ${JSON.stringify(keys)}`);
|
||||
|
||||
const table = useReactTable({
|
||||
data: filteredKeys,
|
||||
|
|
@ -536,27 +538,27 @@ export function AllKeysTable({
|
|||
sorting,
|
||||
},
|
||||
onSortingChange: (updaterOrValue) => {
|
||||
const newSorting = typeof updaterOrValue === "function" ? updaterOrValue(sorting) : updaterOrValue
|
||||
console.log(`newSorting: ${JSON.stringify(newSorting)}`)
|
||||
setSorting(newSorting)
|
||||
const newSorting = typeof updaterOrValue === "function" ? updaterOrValue(sorting) : updaterOrValue;
|
||||
console.log(`newSorting: ${JSON.stringify(newSorting)}`);
|
||||
setSorting(newSorting);
|
||||
if (newSorting && newSorting.length > 0) {
|
||||
const sortState = newSorting[0]
|
||||
const sortBy = sortState.id
|
||||
const sortOrder = sortState.desc ? "desc" : "asc"
|
||||
console.log(`sortBy: ${sortBy}, sortOrder: ${sortOrder}`)
|
||||
const sortState = newSorting[0];
|
||||
const sortBy = sortState.id;
|
||||
const sortOrder = sortState.desc ? "desc" : "asc";
|
||||
console.log(`sortBy: ${sortBy}, sortOrder: ${sortOrder}`);
|
||||
handleFilterChange({
|
||||
...filters,
|
||||
"Sort By": sortBy,
|
||||
"Sort Order": sortOrder,
|
||||
})
|
||||
onSortChange?.(sortBy, sortOrder)
|
||||
});
|
||||
onSortChange?.(sortBy, sortOrder);
|
||||
}
|
||||
},
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
enableSorting: true,
|
||||
manualSorting: false,
|
||||
})
|
||||
});
|
||||
|
||||
// Update local sorting state when currentSort prop changes
|
||||
React.useEffect(() => {
|
||||
|
|
@ -566,9 +568,9 @@ export function AllKeysTable({
|
|||
id: currentSort.sortBy,
|
||||
desc: currentSort.sortOrder === "desc",
|
||||
},
|
||||
])
|
||||
]);
|
||||
}
|
||||
}, [currentSort])
|
||||
}, [currentSort]);
|
||||
|
||||
return (
|
||||
<div className="w-full h-full overflow-hidden">
|
||||
|
|
@ -581,16 +583,16 @@ export function AllKeysTable({
|
|||
setKeys((keys) =>
|
||||
keys.map((key) => {
|
||||
if (key.token === updatedKeyData.token) {
|
||||
return updateExistingKeys(key, updatedKeyData)
|
||||
return updateExistingKeys(key, updatedKeyData);
|
||||
}
|
||||
return key
|
||||
return key;
|
||||
}),
|
||||
)
|
||||
if (refresh) refresh() // Minimal fix: refresh the full key list after an update
|
||||
);
|
||||
if (refresh) refresh(); // Minimal fix: refresh the full key list after an update
|
||||
}}
|
||||
onDelete={() => {
|
||||
setKeys((keys) => keys.filter((key) => key.token !== selectedKeyId))
|
||||
if (refresh) refresh() // Minimal fix: refresh the full key list after a delete
|
||||
setKeys((keys) => keys.filter((key) => key.token !== selectedKeyId));
|
||||
if (refresh) refresh(); // Minimal fix: refresh the full key list after a delete
|
||||
}}
|
||||
accessToken={accessToken}
|
||||
userID={userID}
|
||||
|
|
@ -726,5 +728,5 @@ export function AllKeysTable({
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,19 +25,15 @@ import {
|
|||
TabPanels,
|
||||
Grid,
|
||||
} from "@tremor/react";
|
||||
import { Statistic } from "antd"
|
||||
import { modelAvailableCall } from "./networking";
|
||||
import { Statistic } from "antd";
|
||||
import { modelAvailableCall } from "./networking";
|
||||
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
||||
|
||||
interface ApiRefProps {
|
||||
proxySettings: any;
|
||||
}
|
||||
|
||||
|
||||
const APIRef: React.FC<ApiRefProps> = ({
|
||||
proxySettings,
|
||||
}) => {
|
||||
|
||||
const APIRef: React.FC<ApiRefProps> = ({ proxySettings }) => {
|
||||
let base_url = "<your_proxy_base_url>";
|
||||
|
||||
if (proxySettings) {
|
||||
|
|
@ -45,23 +41,28 @@ const APIRef: React.FC<ApiRefProps> = ({
|
|||
base_url = proxySettings.PROXY_BASE_URL;
|
||||
}
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Grid className="gap-2 p-8 h-[80vh] w-full mt-2">
|
||||
return (
|
||||
<>
|
||||
<Grid className="gap-2 p-8 h-[80vh] w-full mt-2">
|
||||
<div className="mb-5">
|
||||
<p className="text-2xl text-tremor-content-strong dark:text-dark-tremor-content-strong font-semibold">OpenAI Compatible Proxy: API Reference</p>
|
||||
<Text className="mt-2 mb-2">LiteLLM is OpenAI Compatible. This means your API Key works with the OpenAI SDK. Just replace the base_url to point to your litellm proxy. Example Below </Text>
|
||||
<p className="text-2xl text-tremor-content-strong dark:text-dark-tremor-content-strong font-semibold">
|
||||
OpenAI Compatible Proxy: API Reference
|
||||
</p>
|
||||
<Text className="mt-2 mb-2">
|
||||
LiteLLM is OpenAI Compatible. This means your API Key works with the OpenAI SDK. Just replace the base_url
|
||||
to point to your litellm proxy. Example Below{" "}
|
||||
</Text>
|
||||
|
||||
<TabGroup>
|
||||
<TabList>
|
||||
<Tab>OpenAI Python SDK</Tab>
|
||||
<Tab>LlamaIndex</Tab>
|
||||
<Tab>Langchain Py</Tab>
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
<TabPanel>
|
||||
<SyntaxHighlighter language="python">
|
||||
{`
|
||||
<TabGroup>
|
||||
<TabList>
|
||||
<Tab>OpenAI Python SDK</Tab>
|
||||
<Tab>LlamaIndex</Tab>
|
||||
<Tab>Langchain Py</Tab>
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
<TabPanel>
|
||||
<SyntaxHighlighter language="python">
|
||||
{`
|
||||
import openai
|
||||
client = openai.OpenAI(
|
||||
api_key="your_api_key",
|
||||
|
|
@ -80,11 +81,11 @@ response = client.chat.completions.create(
|
|||
|
||||
print(response)
|
||||
`}
|
||||
</SyntaxHighlighter>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<SyntaxHighlighter language="python">
|
||||
{`
|
||||
</SyntaxHighlighter>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<SyntaxHighlighter language="python">
|
||||
{`
|
||||
import os, dotenv
|
||||
|
||||
from llama_index.llms import AzureOpenAI
|
||||
|
|
@ -116,11 +117,11 @@ response = query_engine.query("What did the author do growing up?")
|
|||
print(response)
|
||||
|
||||
`}
|
||||
</SyntaxHighlighter>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<SyntaxHighlighter language="python">
|
||||
{`
|
||||
</SyntaxHighlighter>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<SyntaxHighlighter language="python">
|
||||
{`
|
||||
from langchain.chat_models import ChatOpenAI
|
||||
from langchain.prompts.chat import (
|
||||
ChatPromptTemplate,
|
||||
|
|
@ -148,19 +149,14 @@ response = chat(messages)
|
|||
print(response)
|
||||
|
||||
`}
|
||||
</SyntaxHighlighter>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
|
||||
|
||||
</SyntaxHighlighter>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
</div>
|
||||
</Grid>
|
||||
|
||||
|
||||
</Grid>
|
||||
</>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default APIRef;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,33 +1,33 @@
|
|||
import React, { useState, useRef } from "react"
|
||||
import { QuestionCircleOutlined } from "@ant-design/icons"
|
||||
import React, { useState, useRef } from "react";
|
||||
import { QuestionCircleOutlined } from "@ant-design/icons";
|
||||
|
||||
interface TooltipProps {
|
||||
content: React.ReactNode
|
||||
children?: React.ReactNode
|
||||
width?: string
|
||||
className?: string
|
||||
content: React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
width?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const Tooltip: React.FC<TooltipProps> = ({ content, children, width = "auto", className = "" }) => {
|
||||
const [showTooltip, setShowTooltip] = useState(false)
|
||||
const [tooltipPosition, setTooltipPosition] = useState<"top" | "bottom">("top")
|
||||
const tooltipRef = useRef<HTMLDivElement>(null)
|
||||
const [showTooltip, setShowTooltip] = useState(false);
|
||||
const [tooltipPosition, setTooltipPosition] = useState<"top" | "bottom">("top");
|
||||
const tooltipRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Function to check if tooltip would fit above
|
||||
const checkTooltipPosition = () => {
|
||||
if (tooltipRef.current) {
|
||||
const rect = tooltipRef.current.getBoundingClientRect()
|
||||
const tooltipHeight = 300 // Approximate height of the tooltip
|
||||
const spaceAbove = rect.top
|
||||
const spaceBelow = window.innerHeight - rect.bottom
|
||||
const rect = tooltipRef.current.getBoundingClientRect();
|
||||
const tooltipHeight = 300; // Approximate height of the tooltip
|
||||
const spaceAbove = rect.top;
|
||||
const spaceBelow = window.innerHeight - rect.bottom;
|
||||
|
||||
if (spaceAbove < tooltipHeight && spaceBelow > tooltipHeight) {
|
||||
setTooltipPosition("bottom")
|
||||
setTooltipPosition("bottom");
|
||||
} else {
|
||||
setTooltipPosition("top")
|
||||
setTooltipPosition("top");
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative inline-block" ref={tooltipRef}>
|
||||
|
|
@ -35,8 +35,8 @@ export const Tooltip: React.FC<TooltipProps> = ({ content, children, width = "au
|
|||
<QuestionCircleOutlined
|
||||
className="ml-1 text-gray-500 cursor-help"
|
||||
onMouseEnter={() => {
|
||||
checkTooltipPosition()
|
||||
setShowTooltip(true)
|
||||
checkTooltipPosition();
|
||||
setShowTooltip(true);
|
||||
}}
|
||||
onMouseLeave={() => setShowTooltip(false)}
|
||||
/>
|
||||
|
|
@ -66,5 +66,5 @@ export const Tooltip: React.FC<TooltipProps> = ({ content, children, width = "au
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
export { Tooltip } from './Tooltip';
|
||||
export { Tooltip } from "./Tooltip";
|
||||
|
|
|
|||
|
|
@ -1,22 +1,6 @@
|
|||
import React from "react";
|
||||
import {
|
||||
Button,
|
||||
TextInput,
|
||||
Grid,
|
||||
Col,
|
||||
Accordion,
|
||||
AccordionHeader,
|
||||
AccordionBody,
|
||||
} from "@tremor/react";
|
||||
import {
|
||||
Button as Button2,
|
||||
Modal,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
Select,
|
||||
message,
|
||||
} from "antd";
|
||||
import { Button, TextInput, Grid, Col, Accordion, AccordionHeader, AccordionBody } from "@tremor/react";
|
||||
import { Button as Button2, Modal, Form, Input, InputNumber, Select, message } from "antd";
|
||||
import { budgetCreateCall } from "../networking";
|
||||
import NotificationsManager from "../molecules/notifications_manager";
|
||||
|
||||
|
|
@ -26,12 +10,7 @@ interface BudgetModalProps {
|
|||
setIsModalVisible: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setBudgetList: React.Dispatch<React.SetStateAction<any[]>>;
|
||||
}
|
||||
const BudgetModal: React.FC<BudgetModalProps> = ({
|
||||
isModalVisible,
|
||||
accessToken,
|
||||
setIsModalVisible,
|
||||
setBudgetList,
|
||||
}) => {
|
||||
const BudgetModal: React.FC<BudgetModalProps> = ({ isModalVisible, accessToken, setIsModalVisible, setBudgetList }) => {
|
||||
const [form] = Form.useForm();
|
||||
const handleOk = () => {
|
||||
setIsModalVisible(false);
|
||||
|
|
@ -52,9 +31,7 @@ const BudgetModal: React.FC<BudgetModalProps> = ({
|
|||
// setIsModalVisible(true);
|
||||
const response = await budgetCreateCall(accessToken, formValues);
|
||||
console.log("key create Response:", response);
|
||||
setBudgetList((prevData) =>
|
||||
prevData ? [...prevData, response] : [response]
|
||||
); // Check if prevData is null
|
||||
setBudgetList((prevData) => (prevData ? [...prevData, response] : [response])); // Check if prevData is null
|
||||
NotificationsManager.success("Budget Created");
|
||||
form.resetFields();
|
||||
} catch (error) {
|
||||
|
|
@ -72,13 +49,7 @@ const BudgetModal: React.FC<BudgetModalProps> = ({
|
|||
onOk={handleOk}
|
||||
onCancel={handleCancel}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
onFinish={handleCreate}
|
||||
labelCol={{ span: 8 }}
|
||||
wrapperCol={{ span: 16 }}
|
||||
labelAlign="left"
|
||||
>
|
||||
<Form form={form} onFinish={handleCreate} labelCol={{ span: 8 }} wrapperCol={{ span: 16 }} labelAlign="left">
|
||||
<>
|
||||
<Form.Item
|
||||
label="Budget ID"
|
||||
|
|
@ -93,18 +64,10 @@ const BudgetModal: React.FC<BudgetModalProps> = ({
|
|||
>
|
||||
<TextInput placeholder="" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="Max Tokens per minute"
|
||||
name="tpm_limit"
|
||||
help="Default is model limit."
|
||||
>
|
||||
<Form.Item label="Max Tokens per minute" name="tpm_limit" help="Default is model limit.">
|
||||
<InputNumber step={1} precision={2} width={200} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="Max Requests per minute"
|
||||
name="rpm_limit"
|
||||
help="Default is model limit."
|
||||
>
|
||||
<Form.Item label="Max Requests per minute" name="rpm_limit" help="Default is model limit.">
|
||||
<InputNumber step={1} precision={2} width={200} />
|
||||
</Form.Item>
|
||||
|
||||
|
|
@ -116,11 +79,7 @@ const BudgetModal: React.FC<BudgetModalProps> = ({
|
|||
<Form.Item label="Max Budget (USD)" name="max_budget">
|
||||
<InputNumber step={0.01} precision={2} width={200} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
className="mt-8"
|
||||
label="Reset Budget"
|
||||
name="budget_duration"
|
||||
>
|
||||
<Form.Item className="mt-8" label="Reset Budget" name="budget_duration">
|
||||
<Select defaultValue={null} placeholder="n/a">
|
||||
<Select.Option value="24h">daily</Select.Option>
|
||||
<Select.Option value="7d">weekly</Select.Option>
|
||||
|
|
|
|||
|
|
@ -68,18 +68,18 @@ const BudgetPanel: React.FC<BudgetSettingsPageProps> = ({ accessToken }) => {
|
|||
}, [accessToken]);
|
||||
|
||||
const handleEditCall = async (budget_id: string, index: number) => {
|
||||
console.log("budget_id", budget_id)
|
||||
console.log("budget_id", budget_id);
|
||||
if (accessToken == null) {
|
||||
return;
|
||||
}
|
||||
// Find the budget first
|
||||
const budget = budgetList.find(budget => budget.budget_id === budget_id) || null;
|
||||
|
||||
const budget = budgetList.find((budget) => budget.budget_id === budget_id) || null;
|
||||
|
||||
// Update state and show modal after state is updated
|
||||
setSelectedBudget(budget);
|
||||
setIsEditModalVisible(true);
|
||||
};
|
||||
|
||||
|
||||
const handleDeleteCall = async (budget_id: string, index: number) => {
|
||||
if (accessToken == null) {
|
||||
return;
|
||||
|
|
@ -103,16 +103,11 @@ const BudgetPanel: React.FC<BudgetSettingsPageProps> = ({ accessToken }) => {
|
|||
getBudgetList(accessToken).then((data) => {
|
||||
setBudgetList(data);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full mx-auto flex-auto overflow-y-auto m-8 p-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="primary"
|
||||
className="mb-2"
|
||||
onClick={() => setIsModalVisible(true)}
|
||||
>
|
||||
<Button size="sm" variant="primary" className="mb-2" onClick={() => setIsModalVisible(true)}>
|
||||
+ Create Budget
|
||||
</Button>
|
||||
<BudgetModal
|
||||
|
|
@ -121,8 +116,8 @@ const BudgetPanel: React.FC<BudgetSettingsPageProps> = ({ accessToken }) => {
|
|||
setIsModalVisible={setIsModalVisible}
|
||||
setBudgetList={setBudgetList}
|
||||
/>
|
||||
{
|
||||
selectedBudget && <EditBudgetModal
|
||||
{selectedBudget && (
|
||||
<EditBudgetModal
|
||||
accessToken={accessToken}
|
||||
isModalVisible={isEditModalVisible}
|
||||
setIsModalVisible={setIsEditModalVisible}
|
||||
|
|
@ -130,7 +125,7 @@ const BudgetPanel: React.FC<BudgetSettingsPageProps> = ({ accessToken }) => {
|
|||
existingBudget={selectedBudget}
|
||||
handleUpdateCall={handleUpdateCall}
|
||||
/>
|
||||
}
|
||||
)}
|
||||
<Card>
|
||||
<Text>Create a budget to assign to customers.</Text>
|
||||
<Table>
|
||||
|
|
@ -153,16 +148,8 @@ const BudgetPanel: React.FC<BudgetSettingsPageProps> = ({ accessToken }) => {
|
|||
<TableCell>{value.max_budget ? value.max_budget : "n/a"}</TableCell>
|
||||
<TableCell>{value.tpm_limit ? value.tpm_limit : "n/a"}</TableCell>
|
||||
<TableCell>{value.rpm_limit ? value.rpm_limit : "n/a"}</TableCell>
|
||||
<Icon
|
||||
icon={PencilAltIcon}
|
||||
size="sm"
|
||||
onClick={() => handleEditCall(value.budget_id, index)}
|
||||
/>
|
||||
<Icon
|
||||
icon={TrashIcon}
|
||||
size="sm"
|
||||
onClick={() => handleDeleteCall(value.budget_id, index)}
|
||||
/>
|
||||
<Icon icon={PencilAltIcon} size="sm" onClick={() => handleEditCall(value.budget_id, index)} />
|
||||
<Icon icon={TrashIcon} size="sm" onClick={() => handleDeleteCall(value.budget_id, index)} />
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
|
|
|
|||
|
|
@ -23,24 +23,9 @@ import {
|
|||
AccordionHeader,
|
||||
AccordionList,
|
||||
} from "@tremor/react";
|
||||
import {
|
||||
TabPanel,
|
||||
TabPanels,
|
||||
TabGroup,
|
||||
TabList,
|
||||
Tab,
|
||||
Icon,
|
||||
} from "@tremor/react";
|
||||
import { TabPanel, TabPanels, TabGroup, TabList, Tab, Icon } from "@tremor/react";
|
||||
import { getBudgetSettings } from "../networking";
|
||||
import {
|
||||
Modal,
|
||||
Form,
|
||||
Input,
|
||||
Select,
|
||||
Button as Button2,
|
||||
message,
|
||||
InputNumber,
|
||||
} from "antd";
|
||||
import { Modal, Form, Input, Select, Button as Button2, message, InputNumber } from "antd";
|
||||
import {
|
||||
InformationCircleIcon,
|
||||
PencilAltIcon,
|
||||
|
|
@ -68,9 +53,7 @@ interface budgetSettingsItem {
|
|||
}
|
||||
|
||||
const BudgetSettings: React.FC<BudgetSettingsPageProps> = ({ accessToken }) => {
|
||||
const [budgetSettings, setBudgetSettings] = useState<budgetSettingsItem[]>(
|
||||
[]
|
||||
);
|
||||
const [budgetSettings, setBudgetSettings] = useState<budgetSettingsItem[]>([]);
|
||||
useEffect(() => {
|
||||
if (!accessToken) {
|
||||
return;
|
||||
|
|
@ -85,9 +68,7 @@ const BudgetSettings: React.FC<BudgetSettingsPageProps> = ({ accessToken }) => {
|
|||
const handleInputChange = (fieldName: string, newValue: any) => {
|
||||
// Update the value in the state
|
||||
const updatedSettings = budgetSettings.map((setting) =>
|
||||
setting.field_name === fieldName
|
||||
? { ...setting, field_value: newValue }
|
||||
: setting
|
||||
setting.field_name === fieldName ? { ...setting, field_value: newValue } : setting,
|
||||
);
|
||||
setBudgetSettings(updatedSettings);
|
||||
};
|
||||
|
|
@ -104,9 +85,7 @@ const BudgetSettings: React.FC<BudgetSettingsPageProps> = ({ accessToken }) => {
|
|||
}
|
||||
try {
|
||||
const updatedSettings = budgetSettings.map((setting) =>
|
||||
setting.field_name === fieldName
|
||||
? { ...setting, stored_in_db: true }
|
||||
: setting
|
||||
setting.field_name === fieldName ? { ...setting, stored_in_db: true } : setting,
|
||||
);
|
||||
setBudgetSettings(updatedSettings);
|
||||
} catch (error) {
|
||||
|
|
@ -121,9 +100,7 @@ const BudgetSettings: React.FC<BudgetSettingsPageProps> = ({ accessToken }) => {
|
|||
|
||||
try {
|
||||
const updatedSettings = budgetSettings.map((setting) =>
|
||||
setting.field_name === fieldName
|
||||
? { ...setting, stored_in_db: null, field_value: null }
|
||||
: setting
|
||||
setting.field_name === fieldName ? { ...setting, stored_in_db: null, field_value: null } : setting,
|
||||
);
|
||||
setBudgetSettings(updatedSettings);
|
||||
} catch (error) {
|
||||
|
|
@ -162,23 +139,13 @@ const BudgetSettings: React.FC<BudgetSettingsPageProps> = ({ accessToken }) => {
|
|||
<InputNumber
|
||||
step={1}
|
||||
value={value.field_value}
|
||||
onChange={(newValue) =>
|
||||
handleInputChange(value.field_name, newValue)
|
||||
} // Handle value change
|
||||
onChange={(newValue) => handleInputChange(value.field_name, newValue)} // Handle value change
|
||||
/>
|
||||
) : null}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button
|
||||
onClick={() => handleUpdateField(value.field_name, index)}
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
<Icon
|
||||
icon={TrashIcon}
|
||||
color="red"
|
||||
onClick={() => handleResetField(value.field_name, index)}
|
||||
>
|
||||
<Button onClick={() => handleUpdateField(value.field_name, index)}>Update</Button>
|
||||
<Icon icon={TrashIcon} color="red" onClick={() => handleResetField(value.field_name, index)}>
|
||||
Reset
|
||||
</Icon>
|
||||
</TableCell>
|
||||
|
|
|
|||
|
|
@ -1,22 +1,6 @@
|
|||
import React, { useEffect } from "react";
|
||||
import {
|
||||
Button,
|
||||
TextInput,
|
||||
Grid,
|
||||
Col,
|
||||
Accordion,
|
||||
AccordionHeader,
|
||||
AccordionBody,
|
||||
} from "@tremor/react";
|
||||
import {
|
||||
Button as Button2,
|
||||
Modal,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
Select,
|
||||
message,
|
||||
} from "antd";
|
||||
import { Button, TextInput, Grid, Col, Accordion, AccordionHeader, AccordionBody } from "@tremor/react";
|
||||
import { Button as Button2, Modal, Form, Input, InputNumber, Select, message } from "antd";
|
||||
import { budgetUpdateCall } from "../networking";
|
||||
import { budgetItem } from "./budget_panel";
|
||||
import NotificationsManager from "../molecules/notifications_manager";
|
||||
|
|
@ -26,8 +10,8 @@ interface BudgetModalProps {
|
|||
accessToken: string | null;
|
||||
setIsModalVisible: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setBudgetList: React.Dispatch<React.SetStateAction<any[]>>;
|
||||
existingBudget: budgetItem
|
||||
handleUpdateCall: () => void
|
||||
existingBudget: budgetItem;
|
||||
handleUpdateCall: () => void;
|
||||
}
|
||||
const EditBudgetModal: React.FC<BudgetModalProps> = ({
|
||||
isModalVisible,
|
||||
|
|
@ -35,9 +19,9 @@ const EditBudgetModal: React.FC<BudgetModalProps> = ({
|
|||
setIsModalVisible,
|
||||
setBudgetList,
|
||||
existingBudget,
|
||||
handleUpdateCall
|
||||
handleUpdateCall,
|
||||
}) => {
|
||||
console.log("existingBudget", existingBudget)
|
||||
console.log("existingBudget", existingBudget);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -62,9 +46,7 @@ const EditBudgetModal: React.FC<BudgetModalProps> = ({
|
|||
NotificationsManager.info("Making API Call");
|
||||
setIsModalVisible(true);
|
||||
const response = await budgetUpdateCall(accessToken, formValues);
|
||||
setBudgetList((prevData) =>
|
||||
prevData ? [...prevData, response] : [response]
|
||||
); // Check if prevData is null
|
||||
setBudgetList((prevData) => (prevData ? [...prevData, response] : [response])); // Check if prevData is null
|
||||
NotificationsManager.success("Budget Updated");
|
||||
form.resetFields();
|
||||
handleUpdateCall();
|
||||
|
|
@ -105,18 +87,10 @@ const EditBudgetModal: React.FC<BudgetModalProps> = ({
|
|||
>
|
||||
<TextInput placeholder="" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="Max Tokens per minute"
|
||||
name="tpm_limit"
|
||||
help="Default is model limit."
|
||||
>
|
||||
<Form.Item label="Max Tokens per minute" name="tpm_limit" help="Default is model limit.">
|
||||
<InputNumber step={1} precision={2} width={200} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="Max Requests per minute"
|
||||
name="rpm_limit"
|
||||
help="Default is model limit."
|
||||
>
|
||||
<Form.Item label="Max Requests per minute" name="rpm_limit" help="Default is model limit.">
|
||||
<InputNumber step={1} precision={2} width={200} />
|
||||
</Form.Item>
|
||||
|
||||
|
|
@ -128,11 +102,7 @@ const EditBudgetModal: React.FC<BudgetModalProps> = ({
|
|||
<Form.Item label="Max Budget (USD)" name="max_budget">
|
||||
<InputNumber step={0.01} precision={2} width={200} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
className="mt-8"
|
||||
label="Reset Budget"
|
||||
name="budget_duration"
|
||||
>
|
||||
<Form.Item className="mt-8" label="Reset Budget" name="budget_duration">
|
||||
<Select defaultValue={null} placeholder="n/a">
|
||||
<Select.Option value="24h">daily</Select.Option>
|
||||
<Select.Option value="7d">weekly</Select.Option>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useState, useEffect } from "react"
|
||||
import { Button as TremorButton, Text } from "@tremor/react"
|
||||
import { Modal, Table, Upload, message, Alert, Typography } from "antd"
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Button as TremorButton, Text } from "@tremor/react";
|
||||
import { Modal, Table, Upload, message, Alert, Typography } from "antd";
|
||||
import {
|
||||
UploadOutlined,
|
||||
DownloadOutlined,
|
||||
|
|
@ -8,43 +8,43 @@ import {
|
|||
FileTextOutlined,
|
||||
DeleteOutlined,
|
||||
FileExclamationOutlined,
|
||||
} from "@ant-design/icons"
|
||||
import { userCreateCall, invitationCreateCall, getProxyUISettings } from "./networking"
|
||||
import Papa from "papaparse"
|
||||
import { CheckCircleIcon, XCircleIcon, ExclamationIcon } from "@heroicons/react/outline"
|
||||
import { CopyToClipboard } from "react-copy-to-clipboard"
|
||||
import { InvitationLink } from "./onboarding_link"
|
||||
import NotificationsManager from "./molecules/notifications_manager"
|
||||
} from "@ant-design/icons";
|
||||
import { userCreateCall, invitationCreateCall, getProxyUISettings } from "./networking";
|
||||
import Papa from "papaparse";
|
||||
import { CheckCircleIcon, XCircleIcon, ExclamationIcon } from "@heroicons/react/outline";
|
||||
import { CopyToClipboard } from "react-copy-to-clipboard";
|
||||
import { InvitationLink } from "./onboarding_link";
|
||||
import NotificationsManager from "./molecules/notifications_manager";
|
||||
|
||||
interface BulkCreateUsersProps {
|
||||
accessToken: string
|
||||
teams: any[] | null
|
||||
possibleUIRoles: null | Record<string, Record<string, string>>
|
||||
onUsersCreated?: () => void
|
||||
accessToken: string;
|
||||
teams: any[] | null;
|
||||
possibleUIRoles: null | Record<string, Record<string, string>>;
|
||||
onUsersCreated?: () => void;
|
||||
}
|
||||
|
||||
interface UserData {
|
||||
user_email: string
|
||||
user_role: string
|
||||
teams?: string | string[]
|
||||
metadata?: string
|
||||
max_budget?: string | number
|
||||
budget_duration?: string
|
||||
models?: string | string[]
|
||||
status?: string
|
||||
error?: string
|
||||
rowNumber?: number
|
||||
isValid?: boolean
|
||||
key?: string
|
||||
invitation_link?: string
|
||||
user_email: string;
|
||||
user_role: string;
|
||||
teams?: string | string[];
|
||||
metadata?: string;
|
||||
max_budget?: string | number;
|
||||
budget_duration?: string;
|
||||
models?: string | string[];
|
||||
status?: string;
|
||||
error?: string;
|
||||
rowNumber?: number;
|
||||
isValid?: boolean;
|
||||
key?: string;
|
||||
invitation_link?: string;
|
||||
}
|
||||
|
||||
// Define an interface for the UI settings
|
||||
interface UISettings {
|
||||
PROXY_BASE_URL: string | null
|
||||
PROXY_LOGOUT_URL: string | null
|
||||
DEFAULT_TEAM_DISABLED: boolean
|
||||
SSO_ENABLED: boolean
|
||||
PROXY_BASE_URL: string | null;
|
||||
PROXY_LOGOUT_URL: string | null;
|
||||
DEFAULT_TEAM_DISABLED: boolean;
|
||||
SSO_ENABLED: boolean;
|
||||
}
|
||||
|
||||
const BulkCreateUsersButton: React.FC<BulkCreateUsersProps> = ({
|
||||
|
|
@ -53,113 +53,115 @@ const BulkCreateUsersButton: React.FC<BulkCreateUsersProps> = ({
|
|||
possibleUIRoles,
|
||||
onUsersCreated,
|
||||
}) => {
|
||||
const [isModalVisible, setIsModalVisible] = useState(false)
|
||||
const [parsedData, setParsedData] = useState<UserData[]>([])
|
||||
const [isProcessing, setIsProcessing] = useState(false)
|
||||
const [parseError, setParseError] = useState<string | null>(null)
|
||||
const [csvStructureError, setCsvStructureError] = useState<string | null>(null)
|
||||
const [fileError, setFileError] = useState<string | null>(null)
|
||||
const [selectedFile, setSelectedFile] = useState<File | null>(null)
|
||||
const [uiSettings, setUISettings] = useState<UISettings | null>(null)
|
||||
const [baseUrl, setBaseUrl] = useState("http://localhost:4000")
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
const [parsedData, setParsedData] = useState<UserData[]>([]);
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
const [parseError, setParseError] = useState<string | null>(null);
|
||||
const [csvStructureError, setCsvStructureError] = useState<string | null>(null);
|
||||
const [fileError, setFileError] = useState<string | null>(null);
|
||||
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
||||
const [uiSettings, setUISettings] = useState<UISettings | null>(null);
|
||||
const [baseUrl, setBaseUrl] = useState("http://localhost:4000");
|
||||
|
||||
useEffect(() => {
|
||||
// Get UI settings
|
||||
const fetchUISettings = async () => {
|
||||
try {
|
||||
const uiSettingsResponse = await getProxyUISettings(accessToken)
|
||||
setUISettings(uiSettingsResponse)
|
||||
const uiSettingsResponse = await getProxyUISettings(accessToken);
|
||||
setUISettings(uiSettingsResponse);
|
||||
} catch (error) {
|
||||
console.error("Error fetching UI settings:", error)
|
||||
console.error("Error fetching UI settings:", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fetchUISettings()
|
||||
fetchUISettings();
|
||||
|
||||
// Set base URL
|
||||
const base = new URL("/", window.location.href)
|
||||
setBaseUrl(base.toString())
|
||||
}, [accessToken])
|
||||
const base = new URL("/", window.location.href);
|
||||
setBaseUrl(base.toString());
|
||||
}, [accessToken]);
|
||||
|
||||
const downloadTemplate = () => {
|
||||
const template = [
|
||||
["user_email", "user_role", "teams", "max_budget", "budget_duration", "models"],
|
||||
["user@example.com", "internal_user", "team-id-1,team-id-2", "100", "30d", "gpt-3.5-turbo,gpt-4"],
|
||||
]
|
||||
];
|
||||
|
||||
const csv = Papa.unparse(template)
|
||||
const blob = new Blob([csv], { type: "text/csv" })
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const a = document.createElement("a")
|
||||
a.href = url
|
||||
a.download = "bulk_users_template.csv"
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
window.URL.revokeObjectURL(url)
|
||||
}
|
||||
const csv = Papa.unparse(template);
|
||||
const blob = new Blob([csv], { type: "text/csv" });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = "bulk_users_template.csv";
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
window.URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
const handleFileUpload = (file: File) => {
|
||||
// Reset all error states
|
||||
setParseError(null)
|
||||
setCsvStructureError(null)
|
||||
setFileError(null)
|
||||
setParseError(null);
|
||||
setCsvStructureError(null);
|
||||
setFileError(null);
|
||||
|
||||
// Set the selected file - always show the file even if it's invalid
|
||||
setSelectedFile(file)
|
||||
setSelectedFile(file);
|
||||
|
||||
// Check file type
|
||||
if (file.type !== "text/csv" && !file.name.endsWith(".csv")) {
|
||||
setFileError(`Invalid file type: ${file.name}. Please upload a CSV file (.csv extension).`)
|
||||
NotificationsManager.fromBackend("Invalid file type. Please upload a CSV file.")
|
||||
return false
|
||||
setFileError(`Invalid file type: ${file.name}. Please upload a CSV file (.csv extension).`);
|
||||
NotificationsManager.fromBackend("Invalid file type. Please upload a CSV file.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check file size (limit to 5MB)
|
||||
if (file.size > 5 * 1024 * 1024) {
|
||||
setFileError(
|
||||
`File is too large (${(file.size / (1024 * 1024)).toFixed(1)} MB). Please upload a CSV file smaller than 5MB.`,
|
||||
)
|
||||
return false
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
Papa.parse(file, {
|
||||
complete: (results) => {
|
||||
// Check if file is empty
|
||||
if (!results.data || results.data.length === 0) {
|
||||
setCsvStructureError("The CSV file appears to be empty. Please upload a file with data.")
|
||||
setParsedData([])
|
||||
return
|
||||
setCsvStructureError("The CSV file appears to be empty. Please upload a file with data.");
|
||||
setParsedData([]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if there's only header row
|
||||
if (results.data.length === 1) {
|
||||
setCsvStructureError("The CSV file only contains headers but no user data. Please add user data to your CSV.")
|
||||
setParsedData([])
|
||||
return
|
||||
setCsvStructureError(
|
||||
"The CSV file only contains headers but no user data. Please add user data to your CSV.",
|
||||
);
|
||||
setParsedData([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const headers = results.data[0] as string[]
|
||||
const headers = results.data[0] as string[];
|
||||
|
||||
// Check if headers exist
|
||||
if (headers.length === 0 || (headers.length === 1 && headers[0] === "")) {
|
||||
setCsvStructureError(
|
||||
"The CSV file doesn't contain any column headers. Please make sure your CSV has headers.",
|
||||
)
|
||||
setParsedData([])
|
||||
return
|
||||
);
|
||||
setParsedData([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const requiredColumns = ["user_email", "user_role"]
|
||||
const requiredColumns = ["user_email", "user_role"];
|
||||
|
||||
// Check if all required columns are present
|
||||
const missingColumns = requiredColumns.filter((col) => !headers.includes(col))
|
||||
const missingColumns = requiredColumns.filter((col) => !headers.includes(col));
|
||||
if (missingColumns.length > 0) {
|
||||
setCsvStructureError(
|
||||
`Your CSV is missing these required columns: ${missingColumns.join(", ")}. Please add these columns to your CSV file.`,
|
||||
)
|
||||
setParsedData([])
|
||||
return
|
||||
);
|
||||
setParsedData([]);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
@ -168,7 +170,7 @@ const BulkCreateUsersButton: React.FC<BulkCreateUsersProps> = ({
|
|||
.map((row: any, index: number) => {
|
||||
// Skip empty rows
|
||||
if (row.length === 0 || (row.length === 1 && row[0] === "")) {
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if row has enough columns
|
||||
|
|
@ -179,7 +181,7 @@ const BulkCreateUsersButton: React.FC<BulkCreateUsersProps> = ({
|
|||
error: `Row ${index + 2} has fewer columns than the header row. Please ensure all data is properly formatted.`,
|
||||
user_email: "",
|
||||
user_role: "",
|
||||
} as UserData
|
||||
} as UserData;
|
||||
}
|
||||
|
||||
const user: UserData = {
|
||||
|
|
@ -192,35 +194,35 @@ const BulkCreateUsersButton: React.FC<BulkCreateUsersProps> = ({
|
|||
rowNumber: index + 2,
|
||||
isValid: true,
|
||||
error: "",
|
||||
}
|
||||
};
|
||||
|
||||
// Validate the row
|
||||
const errors: string[] = []
|
||||
const errors: string[] = [];
|
||||
|
||||
// Email validation
|
||||
if (!user.user_email) {
|
||||
errors.push("Email is required")
|
||||
errors.push("Email is required");
|
||||
} else if (!user.user_email.includes("@") || !user.user_email.includes(".")) {
|
||||
errors.push("Invalid email format (must contain @ and domain)")
|
||||
errors.push("Invalid email format (must contain @ and domain)");
|
||||
}
|
||||
|
||||
// Role validation
|
||||
if (!user.user_role) {
|
||||
errors.push("Role is required")
|
||||
errors.push("Role is required");
|
||||
} else {
|
||||
// Validate user role
|
||||
const validRoles = ["proxy_admin", "proxy_admin_view_only", "internal_user", "internal_user_view_only"]
|
||||
const validRoles = ["proxy_admin", "proxy_admin_view_only", "internal_user", "internal_user_view_only"];
|
||||
if (!validRoles.includes(user.user_role)) {
|
||||
errors.push(`Invalid role "${user.user_role}". Must be one of: ${validRoles.join(", ")}`)
|
||||
errors.push(`Invalid role "${user.user_role}". Must be one of: ${validRoles.join(", ")}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Budget validation
|
||||
if (user.max_budget && user.max_budget.toString().trim() !== "") {
|
||||
if (isNaN(parseFloat(user.max_budget.toString()))) {
|
||||
errors.push(`Max budget "${user.max_budget}" must be a number`)
|
||||
errors.push(`Max budget "${user.max_budget}" must be a number`);
|
||||
} else if (parseFloat(user.max_budget.toString()) <= 0) {
|
||||
errors.push("Max budget must be greater than 0")
|
||||
errors.push("Max budget must be greater than 0");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -228,93 +230,93 @@ const BulkCreateUsersButton: React.FC<BulkCreateUsersProps> = ({
|
|||
if (user.budget_duration && !user.budget_duration.match(/^\d+[dhmwy]$|^\d+mo$/)) {
|
||||
errors.push(
|
||||
`Invalid budget duration format "${user.budget_duration}". Use format like "30d", "1mo", "2w", "6h"`,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Teams validation
|
||||
if (user.teams && typeof user.teams === "string") {
|
||||
// Check if teams exist (if teams data is available)
|
||||
if (teams && teams.length > 0) {
|
||||
const teamIds = teams.map((t) => t.team_id)
|
||||
const userTeams = user.teams.split(",").map((t) => t.trim())
|
||||
const invalidTeams = userTeams.filter((t) => !teamIds.includes(t))
|
||||
const teamIds = teams.map((t) => t.team_id);
|
||||
const userTeams = user.teams.split(",").map((t) => t.trim());
|
||||
const invalidTeams = userTeams.filter((t) => !teamIds.includes(t));
|
||||
if (invalidTeams.length > 0) {
|
||||
errors.push(`Unknown team(s): ${invalidTeams.join(", ")}`)
|
||||
errors.push(`Unknown team(s): ${invalidTeams.join(", ")}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
user.isValid = false
|
||||
user.error = errors.join(", ")
|
||||
user.isValid = false;
|
||||
user.error = errors.join(", ");
|
||||
}
|
||||
|
||||
return user
|
||||
return user;
|
||||
})
|
||||
.filter(Boolean) as UserData[] // Filter out null values (empty rows)
|
||||
.filter(Boolean) as UserData[]; // Filter out null values (empty rows)
|
||||
|
||||
const validData = userData.filter((user) => user.isValid)
|
||||
setParsedData(userData)
|
||||
const validData = userData.filter((user) => user.isValid);
|
||||
setParsedData(userData);
|
||||
|
||||
if (userData.length === 0) {
|
||||
setCsvStructureError("No valid data rows found in the CSV file. Please check your file format.")
|
||||
setCsvStructureError("No valid data rows found in the CSV file. Please check your file format.");
|
||||
} else if (validData.length === 0) {
|
||||
setParseError("No valid users found in the CSV. Please check the errors below and fix your CSV file.")
|
||||
setParseError("No valid users found in the CSV. Please check the errors below and fix your CSV file.");
|
||||
} else if (validData.length < userData.length) {
|
||||
setParseError(
|
||||
`Found ${userData.length - validData.length} row(s) with errors out of ${userData.length} total rows. Please correct them before proceeding.`,
|
||||
)
|
||||
);
|
||||
} else {
|
||||
NotificationsManager.success(`Successfully parsed ${validData.length} users`)
|
||||
NotificationsManager.success(`Successfully parsed ${validData.length} users`);
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
const errorMessage = error instanceof Error ? error.message : "Unknown error"
|
||||
setParseError(`Error parsing CSV: ${errorMessage}`)
|
||||
setParsedData([])
|
||||
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
||||
setParseError(`Error parsing CSV: ${errorMessage}`);
|
||||
setParsedData([]);
|
||||
}
|
||||
},
|
||||
error: (error) => {
|
||||
setParseError(`Failed to parse CSV file: ${error.message}`)
|
||||
setParsedData([])
|
||||
setParseError(`Failed to parse CSV file: ${error.message}`);
|
||||
setParsedData([]);
|
||||
},
|
||||
header: false,
|
||||
})
|
||||
return false
|
||||
}
|
||||
});
|
||||
return false;
|
||||
};
|
||||
|
||||
const removeSelectedFile = () => {
|
||||
setSelectedFile(null)
|
||||
setParsedData([])
|
||||
setParseError(null)
|
||||
setCsvStructureError(null)
|
||||
setFileError(null)
|
||||
}
|
||||
setSelectedFile(null);
|
||||
setParsedData([]);
|
||||
setParseError(null);
|
||||
setCsvStructureError(null);
|
||||
setFileError(null);
|
||||
};
|
||||
|
||||
const handleBulkCreate = async () => {
|
||||
setIsProcessing(true)
|
||||
const updatedData = parsedData.map((user) => ({ ...user, status: "pending" }))
|
||||
setParsedData(updatedData)
|
||||
setIsProcessing(true);
|
||||
const updatedData = parsedData.map((user) => ({ ...user, status: "pending" }));
|
||||
setParsedData(updatedData);
|
||||
|
||||
let anySuccessful = false
|
||||
let anySuccessful = false;
|
||||
|
||||
for (let index = 0; index < updatedData.length; index++) {
|
||||
const user = updatedData[index]
|
||||
const user = updatedData[index];
|
||||
try {
|
||||
// Create a clean user object with only non-empty values
|
||||
const cleanUser: Partial<UserData> = {
|
||||
user_email: user.user_email,
|
||||
user_role: user.user_role,
|
||||
}
|
||||
};
|
||||
|
||||
// Only add optional fields if they have values
|
||||
if (user.teams && typeof user.teams === "string" && user.teams.trim() !== "") {
|
||||
cleanUser.teams = user.teams
|
||||
.split(",")
|
||||
.map((team) => team.trim())
|
||||
.filter(Boolean)
|
||||
.filter(Boolean);
|
||||
// Only include teams if there's at least one valid team
|
||||
if (cleanUser.teams.length === 0) {
|
||||
delete cleanUser.teams
|
||||
delete cleanUser.teams;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -323,47 +325,47 @@ const BulkCreateUsersButton: React.FC<BulkCreateUsersProps> = ({
|
|||
cleanUser.models = user.models
|
||||
.split(",")
|
||||
.map((model) => model.trim())
|
||||
.filter(Boolean)
|
||||
.filter(Boolean);
|
||||
// Only include models if there's at least one valid model
|
||||
if (cleanUser.models.length === 0) {
|
||||
delete cleanUser.models
|
||||
delete cleanUser.models;
|
||||
}
|
||||
}
|
||||
|
||||
// Only add max_budget if it's a valid number
|
||||
if (user.max_budget && user.max_budget.toString().trim() !== "") {
|
||||
const budgetValue = parseFloat(user.max_budget.toString())
|
||||
const budgetValue = parseFloat(user.max_budget.toString());
|
||||
if (!isNaN(budgetValue) && budgetValue > 0) {
|
||||
cleanUser.max_budget = budgetValue
|
||||
cleanUser.max_budget = budgetValue;
|
||||
}
|
||||
}
|
||||
|
||||
// Only add budget_duration if provided and non-empty
|
||||
if (user.budget_duration && user.budget_duration.trim() !== "") {
|
||||
cleanUser.budget_duration = user.budget_duration.trim()
|
||||
cleanUser.budget_duration = user.budget_duration.trim();
|
||||
}
|
||||
|
||||
// Only add metadata if provided and non-empty
|
||||
if (user.metadata && typeof user.metadata === "string" && user.metadata.trim() !== "") {
|
||||
cleanUser.metadata = user.metadata.trim()
|
||||
cleanUser.metadata = user.metadata.trim();
|
||||
}
|
||||
|
||||
console.log("Sending user data:", cleanUser)
|
||||
const response = await userCreateCall(accessToken, null, cleanUser)
|
||||
console.log("Full response:", response)
|
||||
console.log("Sending user data:", cleanUser);
|
||||
const response = await userCreateCall(accessToken, null, cleanUser);
|
||||
console.log("Full response:", response);
|
||||
|
||||
// Check if response has key or user_id, indicating success
|
||||
if (response && (response.key || response.user_id)) {
|
||||
anySuccessful = true
|
||||
console.log("Success case triggered")
|
||||
const user_id = response.data?.user_id || response.user_id
|
||||
anySuccessful = true;
|
||||
console.log("Success case triggered");
|
||||
const user_id = response.data?.user_id || response.user_id;
|
||||
|
||||
// Create invitation link for the user
|
||||
try {
|
||||
if (!uiSettings?.SSO_ENABLED) {
|
||||
// Regular invitation flow
|
||||
const invitationData = await invitationCreateCall(accessToken, user_id)
|
||||
const invitationUrl = new URL(`/ui?invitation_id=${invitationData.id}`, baseUrl).toString()
|
||||
const invitationData = await invitationCreateCall(accessToken, user_id);
|
||||
const invitationUrl = new URL(`/ui?invitation_id=${invitationData.id}`, baseUrl).toString();
|
||||
|
||||
setParsedData((current) =>
|
||||
current.map((u, i) =>
|
||||
|
|
@ -376,10 +378,10 @@ const BulkCreateUsersButton: React.FC<BulkCreateUsersProps> = ({
|
|||
}
|
||||
: u,
|
||||
),
|
||||
)
|
||||
);
|
||||
} else {
|
||||
// SSO flow - just use the base URL
|
||||
const invitationUrl = new URL("/ui", baseUrl).toString()
|
||||
const invitationUrl = new URL("/ui", baseUrl).toString();
|
||||
|
||||
setParsedData((current) =>
|
||||
current.map((u, i) =>
|
||||
|
|
@ -392,10 +394,10 @@ const BulkCreateUsersButton: React.FC<BulkCreateUsersProps> = ({
|
|||
}
|
||||
: u,
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
} catch (inviteError) {
|
||||
console.error("Error creating invitation:", inviteError)
|
||||
console.error("Error creating invitation:", inviteError);
|
||||
setParsedData((current) =>
|
||||
current.map((u, i) =>
|
||||
i === index
|
||||
|
|
@ -407,32 +409,32 @@ const BulkCreateUsersButton: React.FC<BulkCreateUsersProps> = ({
|
|||
}
|
||||
: u,
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
console.log("Error case triggered")
|
||||
const errorMessage = response?.error || "Failed to create user"
|
||||
console.log("Error message:", errorMessage)
|
||||
console.log("Error case triggered");
|
||||
const errorMessage = response?.error || "Failed to create user";
|
||||
console.log("Error message:", errorMessage);
|
||||
setParsedData((current) =>
|
||||
current.map((u, i) => (i === index ? { ...u, status: "failed", error: errorMessage } : u)),
|
||||
)
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Caught error:", error)
|
||||
const errorMessage = (error as any)?.response?.data?.error || (error as Error)?.message || String(error)
|
||||
console.error("Caught error:", error);
|
||||
const errorMessage = (error as any)?.response?.data?.error || (error as Error)?.message || String(error);
|
||||
setParsedData((current) =>
|
||||
current.map((u, i) => (i === index ? { ...u, status: "failed", error: errorMessage } : u)),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
setIsProcessing(false)
|
||||
setIsProcessing(false);
|
||||
|
||||
// Call the callback if any users were successfully created
|
||||
if (anySuccessful && onUsersCreated) {
|
||||
onUsersCreated()
|
||||
onUsersCreated();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const downloadResults = () => {
|
||||
const results = parsedData.map((user) => ({
|
||||
|
|
@ -442,19 +444,19 @@ const BulkCreateUsersButton: React.FC<BulkCreateUsersProps> = ({
|
|||
key: user.key || "",
|
||||
invitation_link: user.invitation_link || "",
|
||||
error: user.error || "",
|
||||
}))
|
||||
}));
|
||||
|
||||
const csv = Papa.unparse(results)
|
||||
const blob = new Blob([csv], { type: "text/csv" })
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const a = document.createElement("a")
|
||||
a.href = url
|
||||
a.download = "bulk_users_results.csv"
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
window.URL.revokeObjectURL(url)
|
||||
}
|
||||
const csv = Papa.unparse(results);
|
||||
const blob = new Blob([csv], { type: "text/csv" });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = "bulk_users_results.csv";
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
window.URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
|
|
@ -496,10 +498,10 @@ const BulkCreateUsersButton: React.FC<BulkCreateUsersProps> = ({
|
|||
</div>
|
||||
{record.error && <span className="text-sm text-red-500 ml-7">{record.error}</span>}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
if (!record.status || record.status === "pending") {
|
||||
return <span className="text-gray-500">Pending</span>
|
||||
return <span className="text-gray-500">Pending</span>;
|
||||
}
|
||||
if (record.status === "success") {
|
||||
return (
|
||||
|
|
@ -522,7 +524,7 @@ const BulkCreateUsersButton: React.FC<BulkCreateUsersProps> = ({
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
|
|
@ -532,10 +534,10 @@ const BulkCreateUsersButton: React.FC<BulkCreateUsersProps> = ({
|
|||
</div>
|
||||
{record.error && <span className="text-sm text-red-500 ml-7">{JSON.stringify(record.error)}</span>}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
},
|
||||
},
|
||||
]
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -782,8 +784,8 @@ const BulkCreateUsersButton: React.FC<BulkCreateUsersProps> = ({
|
|||
<div className="flex space-x-3">
|
||||
<TremorButton
|
||||
onClick={() => {
|
||||
setParsedData([])
|
||||
setParseError(null)
|
||||
setParsedData([]);
|
||||
setParseError(null);
|
||||
}}
|
||||
variant="secondary"
|
||||
>
|
||||
|
|
@ -830,8 +832,8 @@ const BulkCreateUsersButton: React.FC<BulkCreateUsersProps> = ({
|
|||
<div className="flex justify-end mt-4">
|
||||
<TremorButton
|
||||
onClick={() => {
|
||||
setParsedData([])
|
||||
setParseError(null)
|
||||
setParsedData([]);
|
||||
setParseError(null);
|
||||
}}
|
||||
variant="secondary"
|
||||
className="mr-3"
|
||||
|
|
@ -851,8 +853,8 @@ const BulkCreateUsersButton: React.FC<BulkCreateUsersProps> = ({
|
|||
<div className="flex justify-end mt-4">
|
||||
<TremorButton
|
||||
onClick={() => {
|
||||
setParsedData([])
|
||||
setParseError(null)
|
||||
setParsedData([]);
|
||||
setParseError(null);
|
||||
}}
|
||||
variant="secondary"
|
||||
className="mr-3"
|
||||
|
|
@ -870,7 +872,7 @@ const BulkCreateUsersButton: React.FC<BulkCreateUsersProps> = ({
|
|||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default BulkCreateUsersButton
|
||||
export default BulkCreateUsersButton;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState } from 'react';
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
Button as Button2,
|
||||
Modal,
|
||||
|
|
@ -13,7 +13,7 @@ import {
|
|||
Space,
|
||||
Checkbox,
|
||||
} from "antd";
|
||||
import { Button } from '@tremor/react';
|
||||
import { Button } from "@tremor/react";
|
||||
import { userBulkUpdateUserCall, teamBulkMemberAddCall, Member } from "./networking";
|
||||
import { UserEditView } from "./user_edit_view";
|
||||
import NotificationsManager from "./molecules/notifications_manager";
|
||||
|
|
@ -61,22 +61,25 @@ const BulkEditUserModal: React.FC<BulkEditUserModalProps> = ({
|
|||
};
|
||||
|
||||
// Create a mock userData object for the UserEditView
|
||||
const mockUserData = React.useMemo(() => ({
|
||||
user_id: "bulk_edit",
|
||||
user_info: {
|
||||
user_email: "",
|
||||
user_role: "",
|
||||
teams: [],
|
||||
models: [],
|
||||
max_budget: null,
|
||||
spend: 0,
|
||||
metadata: {},
|
||||
created_at: null,
|
||||
updated_at: null,
|
||||
},
|
||||
keys: [],
|
||||
teams: teams || [],
|
||||
}), [teams, visible]);
|
||||
const mockUserData = React.useMemo(
|
||||
() => ({
|
||||
user_id: "bulk_edit",
|
||||
user_info: {
|
||||
user_email: "",
|
||||
user_role: "",
|
||||
teams: [],
|
||||
models: [],
|
||||
max_budget: null,
|
||||
spend: 0,
|
||||
metadata: {},
|
||||
created_at: null,
|
||||
updated_at: null,
|
||||
},
|
||||
keys: [],
|
||||
teams: teams || [],
|
||||
}),
|
||||
[teams, visible],
|
||||
);
|
||||
|
||||
const handleSubmit = async (formValues: any) => {
|
||||
console.log("formValues", formValues);
|
||||
|
|
@ -87,15 +90,15 @@ const BulkEditUserModal: React.FC<BulkEditUserModalProps> = ({
|
|||
|
||||
setLoading(true);
|
||||
try {
|
||||
const userIds = selectedUsers.map(user => user.user_id);
|
||||
|
||||
const userIds = selectedUsers.map((user) => user.user_id);
|
||||
|
||||
// Build the update payload - only include fields that have been changed from default/empty values
|
||||
const updatePayload: any = {};
|
||||
|
||||
|
||||
if (formValues.user_role && formValues.user_role !== "") {
|
||||
updatePayload.user_role = formValues.user_role;
|
||||
}
|
||||
|
||||
|
||||
if (formValues.max_budget !== null && formValues.max_budget !== undefined) {
|
||||
updatePayload.max_budget = formValues.max_budget;
|
||||
}
|
||||
|
|
@ -137,15 +140,15 @@ const BulkEditUserModal: React.FC<BulkEditUserModalProps> = ({
|
|||
// Handle team additions
|
||||
if (hasTeamAdditions) {
|
||||
const teamResults: any[] = [];
|
||||
|
||||
|
||||
for (const teamId of selectedTeams) {
|
||||
try {
|
||||
// Create member objects for bulk add
|
||||
let members: Member[] | null = null;
|
||||
if (updateAllUsers) {
|
||||
members = null;
|
||||
} else {
|
||||
const members = selectedUsers.map(user => ({
|
||||
} else {
|
||||
const members = selectedUsers.map((user) => ({
|
||||
user_id: user.user_id,
|
||||
role: "user" as const, // Default role for bulk add
|
||||
user_email: user.user_email || null,
|
||||
|
|
@ -157,11 +160,11 @@ const BulkEditUserModal: React.FC<BulkEditUserModalProps> = ({
|
|||
teamId,
|
||||
members ? members : null,
|
||||
teamBudget || undefined,
|
||||
updateAllUsers
|
||||
updateAllUsers,
|
||||
);
|
||||
|
||||
console.log("result", result);
|
||||
|
||||
|
||||
teamResults.push({
|
||||
teamId,
|
||||
success: true,
|
||||
|
|
@ -179,29 +182,29 @@ const BulkEditUserModal: React.FC<BulkEditUserModalProps> = ({
|
|||
}
|
||||
|
||||
// Generate team success message
|
||||
const successfulTeams = teamResults.filter(r => r.success);
|
||||
const failedTeams = teamResults.filter(r => !r.success);
|
||||
|
||||
const successfulTeams = teamResults.filter((r) => r.success);
|
||||
const failedTeams = teamResults.filter((r) => !r.success);
|
||||
|
||||
if (successfulTeams.length > 0) {
|
||||
const totalAdditions = successfulTeams.reduce((sum, r) => sum + r.successfulAdditions, 0);
|
||||
successMessages.push(`Added users to ${successfulTeams.length} team(s) (${totalAdditions} total additions)`);
|
||||
}
|
||||
|
||||
|
||||
if (failedTeams.length > 0) {
|
||||
message.warning(`Failed to add users to ${failedTeams.length} team(s)`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (successMessages.length > 0) {
|
||||
NotificationsManager.success(successMessages.join('. '));
|
||||
NotificationsManager.success(successMessages.join(". "));
|
||||
}
|
||||
|
||||
|
||||
// Reset team management state
|
||||
setSelectedTeams([]);
|
||||
setTeamBudget(null);
|
||||
setAddToTeams(false);
|
||||
setUpdateAllUsers(false);
|
||||
|
||||
|
||||
onSuccess();
|
||||
onCancel();
|
||||
} catch (error) {
|
||||
|
|
@ -222,79 +225,72 @@ const BulkEditUserModal: React.FC<BulkEditUserModalProps> = ({
|
|||
>
|
||||
{allowAllUsers && (
|
||||
<div className="mb-4">
|
||||
<Checkbox
|
||||
checked={updateAllUsers}
|
||||
onChange={(e) => setUpdateAllUsers(e.target.checked)}
|
||||
>
|
||||
<Checkbox checked={updateAllUsers} onChange={(e) => setUpdateAllUsers(e.target.checked)}>
|
||||
<Text strong>Update ALL users in the system</Text>
|
||||
</Checkbox>
|
||||
{updateAllUsers && (
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<Text type="warning" style={{ fontSize: '12px' }}>
|
||||
<Text type="warning" style={{ fontSize: "12px" }}>
|
||||
⚠️ This will apply changes to ALL users in the system, not just the selected ones.
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{!updateAllUsers && (
|
||||
<div className="mb-4">
|
||||
<Title level={5}>Selected Users ({selectedUsers.length}):</Title>
|
||||
<Table
|
||||
size="small"
|
||||
bordered
|
||||
dataSource={selectedUsers}
|
||||
pagination={false}
|
||||
scroll={{ y: 200 }}
|
||||
rowKey="user_id"
|
||||
columns={[
|
||||
{
|
||||
title: 'User ID',
|
||||
dataIndex: 'user_id',
|
||||
key: 'user_id',
|
||||
width: '30%',
|
||||
render: (text: string) => (
|
||||
<Text strong style={{ fontSize: '12px' }}>
|
||||
{text.length > 20 ? `${text.slice(0, 20)}...` : text}
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Email',
|
||||
dataIndex: 'user_email',
|
||||
key: 'user_email',
|
||||
width: '25%',
|
||||
render: (text: string) => (
|
||||
<Text type="secondary" style={{ fontSize: '12px' }}>
|
||||
{text || 'No email'}
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Current Role',
|
||||
dataIndex: 'user_role',
|
||||
key: 'user_role',
|
||||
width: '25%',
|
||||
render: (role: string) => (
|
||||
<Text style={{ fontSize: '12px' }}>
|
||||
{possibleUIRoles?.[role]?.ui_label || role}
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Budget',
|
||||
dataIndex: 'max_budget',
|
||||
key: 'max_budget',
|
||||
width: '20%',
|
||||
render: (budget: number | null) => (
|
||||
<Text style={{ fontSize: '12px' }}>
|
||||
{budget !== null ? `$${budget}` : 'Unlimited'}
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Table
|
||||
size="small"
|
||||
bordered
|
||||
dataSource={selectedUsers}
|
||||
pagination={false}
|
||||
scroll={{ y: 200 }}
|
||||
rowKey="user_id"
|
||||
columns={[
|
||||
{
|
||||
title: "User ID",
|
||||
dataIndex: "user_id",
|
||||
key: "user_id",
|
||||
width: "30%",
|
||||
render: (text: string) => (
|
||||
<Text strong style={{ fontSize: "12px" }}>
|
||||
{text.length > 20 ? `${text.slice(0, 20)}...` : text}
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Email",
|
||||
dataIndex: "user_email",
|
||||
key: "user_email",
|
||||
width: "25%",
|
||||
render: (text: string) => (
|
||||
<Text type="secondary" style={{ fontSize: "12px" }}>
|
||||
{text || "No email"}
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Current Role",
|
||||
dataIndex: "user_role",
|
||||
key: "user_role",
|
||||
width: "25%",
|
||||
render: (role: string) => (
|
||||
<Text style={{ fontSize: "12px" }}>{possibleUIRoles?.[role]?.ui_label || role}</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Budget",
|
||||
dataIndex: "max_budget",
|
||||
key: "max_budget",
|
||||
width: "20%",
|
||||
render: (budget: number | null) => (
|
||||
<Text style={{ fontSize: "12px" }}>{budget !== null ? `$${budget}` : "Unlimited"}</Text>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
@ -302,26 +298,18 @@ const BulkEditUserModal: React.FC<BulkEditUserModalProps> = ({
|
|||
|
||||
<div className="mb-4">
|
||||
<Text>
|
||||
<strong>Instructions:</strong> Fill in the fields below with the values you want to apply to all selected users.
|
||||
You can bulk edit: role, budget, models, and metadata. You can also add users to teams.
|
||||
<strong>Instructions:</strong> Fill in the fields below with the values you want to apply to all selected
|
||||
users. You can bulk edit: role, budget, models, and metadata. You can also add users to teams.
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{/* Team Management Section */}
|
||||
<Card
|
||||
title="Team Management"
|
||||
size="small"
|
||||
className="mb-4"
|
||||
style={{ backgroundColor: '#fafafa' }}
|
||||
>
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<Checkbox
|
||||
checked={addToTeams}
|
||||
onChange={(e) => setAddToTeams(e.target.checked)}
|
||||
>
|
||||
<Card title="Team Management" size="small" className="mb-4" style={{ backgroundColor: "#fafafa" }}>
|
||||
<Space direction="vertical" style={{ width: "100%" }}>
|
||||
<Checkbox checked={addToTeams} onChange={(e) => setAddToTeams(e.target.checked)}>
|
||||
Add selected users to teams
|
||||
</Checkbox>
|
||||
|
||||
|
||||
{addToTeams && (
|
||||
<>
|
||||
<div>
|
||||
|
|
@ -331,32 +319,35 @@ const BulkEditUserModal: React.FC<BulkEditUserModalProps> = ({
|
|||
placeholder="Select teams to add users to"
|
||||
value={selectedTeams}
|
||||
onChange={setSelectedTeams}
|
||||
style={{ width: '100%', marginTop: 8 }}
|
||||
options={teams?.map(team => ({
|
||||
label: team.team_alias || team.team_id,
|
||||
value: team.team_id,
|
||||
})) || []}
|
||||
style={{ width: "100%", marginTop: 8 }}
|
||||
options={
|
||||
teams?.map((team) => ({
|
||||
label: team.team_alias || team.team_id,
|
||||
value: team.team_id,
|
||||
})) || []
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<Text strong>Team Budget (Optional):</Text>
|
||||
<InputNumber
|
||||
placeholder="Max budget per user in team"
|
||||
value={teamBudget}
|
||||
onChange={(value) => setTeamBudget(value)}
|
||||
style={{ width: '100%', marginTop: 8 }}
|
||||
style={{ width: "100%", marginTop: 8 }}
|
||||
min={0}
|
||||
step={0.01}
|
||||
precision={2}
|
||||
/>
|
||||
<Text type="secondary" style={{ fontSize: '12px' }}>
|
||||
<Text type="secondary" style={{ fontSize: "12px" }}>
|
||||
Leave empty for unlimited budget within team limits
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<Text type="secondary" style={{ fontSize: '12px' }}>
|
||||
Users will be added with "user" role by default. All users will be added to each selected team.
|
||||
|
||||
<Text type="secondary" style={{ fontSize: "12px" }}>
|
||||
Users will be added with "user" role by default. All users will be added to each selected
|
||||
team.
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
|
|
@ -385,4 +376,4 @@ const BulkEditUserModal: React.FC<BulkEditUserModalProps> = ({
|
|||
);
|
||||
};
|
||||
|
||||
export default BulkEditUserModal;
|
||||
export default BulkEditUserModal;
|
||||
|
|
|
|||
|
|
@ -25,68 +25,54 @@ import {
|
|||
import UsageDatePicker from "./shared/usage_date_picker";
|
||||
import NotificationsManager from "./molecules/notifications_manager";
|
||||
|
||||
import {
|
||||
Button as Button2,
|
||||
message,
|
||||
} from "antd";
|
||||
import {
|
||||
RefreshIcon,
|
||||
CheckCircleIcon,
|
||||
XCircleIcon,
|
||||
} from "@heroicons/react/outline";
|
||||
import {
|
||||
adminGlobalCacheActivity,
|
||||
cachingHealthCheckCall,
|
||||
healthCheckCall,
|
||||
} from "./networking";
|
||||
import { Button as Button2, message } from "antd";
|
||||
import { RefreshIcon, CheckCircleIcon, XCircleIcon } from "@heroicons/react/outline";
|
||||
import { adminGlobalCacheActivity, cachingHealthCheckCall, healthCheckCall } from "./networking";
|
||||
|
||||
// Import the new component
|
||||
import { CacheHealthTab } from "./cache_health";
|
||||
|
||||
const formatDateWithoutTZ = (date: Date | undefined) => {
|
||||
if (!date) return undefined;
|
||||
return date.toISOString().split('T')[0];
|
||||
};
|
||||
|
||||
if (!date) return undefined;
|
||||
return date.toISOString().split("T")[0];
|
||||
};
|
||||
|
||||
function valueFormatterNumbers(number: number) {
|
||||
const formatter = new Intl.NumberFormat('en-US', {
|
||||
const formatter = new Intl.NumberFormat("en-US", {
|
||||
maximumFractionDigits: 0,
|
||||
notation: 'compact',
|
||||
compactDisplay: 'short',
|
||||
});
|
||||
notation: "compact",
|
||||
compactDisplay: "short",
|
||||
});
|
||||
|
||||
return formatter.format(number);
|
||||
return formatter.format(number);
|
||||
}
|
||||
|
||||
interface CachePageProps {
|
||||
accessToken: string | null;
|
||||
token: string | null;
|
||||
userRole: string | null;
|
||||
userID: string | null;
|
||||
premiumUser: boolean;
|
||||
accessToken: string | null;
|
||||
token: string | null;
|
||||
userRole: string | null;
|
||||
userID: string | null;
|
||||
premiumUser: boolean;
|
||||
}
|
||||
|
||||
interface cacheDataItem {
|
||||
api_key: string;
|
||||
model: string;
|
||||
cache_hit_true_rows: number;
|
||||
cached_completion_tokens: number;
|
||||
total_rows: number;
|
||||
generated_completion_tokens: number;
|
||||
call_type: string;
|
||||
|
||||
// Add other properties as needed
|
||||
}
|
||||
api_key: string;
|
||||
model: string;
|
||||
cache_hit_true_rows: number;
|
||||
cached_completion_tokens: number;
|
||||
total_rows: number;
|
||||
generated_completion_tokens: number;
|
||||
call_type: string;
|
||||
|
||||
// Add other properties as needed
|
||||
}
|
||||
|
||||
interface uiData {
|
||||
"name": string;
|
||||
"LLM API requests": number;
|
||||
"Cache hit": number;
|
||||
"Cached Completion Tokens": number;
|
||||
"Generated Completion Tokens": number;
|
||||
|
||||
name: string;
|
||||
"LLM API requests": number;
|
||||
"Cache hit": number;
|
||||
"Cached Completion Tokens": number;
|
||||
"Generated Completion Tokens": number;
|
||||
}
|
||||
|
||||
interface CacheHealthResponse {
|
||||
|
|
@ -116,23 +102,17 @@ const deepParse = (input: any) => {
|
|||
return parsed;
|
||||
};
|
||||
|
||||
const CacheDashboard: React.FC<CachePageProps> = ({
|
||||
accessToken,
|
||||
token,
|
||||
userRole,
|
||||
userID,
|
||||
premiumUser,
|
||||
}) => {
|
||||
const CacheDashboard: React.FC<CachePageProps> = ({ accessToken, token, userRole, userID, premiumUser }) => {
|
||||
const [filteredData, setFilteredData] = useState<uiData[]>([]);
|
||||
const [selectedApiKeys, setSelectedApiKeys] = useState<string[]>([]);
|
||||
const [selectedModels, setSelectedModels] = useState<string[]>([]);
|
||||
const [selectedModels, setSelectedModels] = useState<string[]>([]);
|
||||
const [data, setData] = useState<cacheDataItem[]>([]);
|
||||
const [cachedResponses, setCachedResponses] = useState("0");
|
||||
const [cachedTokens, setCachedTokens] = useState("0");
|
||||
const [cacheHitRatio, setCacheHitRatio] = useState("0");
|
||||
|
||||
const [dateValue, setDateValue] = useState<DateRangePickerValue>({
|
||||
from: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000),
|
||||
from: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000),
|
||||
to: new Date(),
|
||||
});
|
||||
|
||||
|
|
@ -144,7 +124,11 @@ const CacheDashboard: React.FC<CachePageProps> = ({
|
|||
return;
|
||||
}
|
||||
const fetchData = async () => {
|
||||
const response = await adminGlobalCacheActivity(accessToken, formatDateWithoutTZ(dateValue.from), formatDateWithoutTZ(dateValue.to));
|
||||
const response = await adminGlobalCacheActivity(
|
||||
accessToken,
|
||||
formatDateWithoutTZ(dateValue.from),
|
||||
formatDateWithoutTZ(dateValue.to),
|
||||
);
|
||||
setData(response);
|
||||
};
|
||||
fetchData();
|
||||
|
|
@ -153,12 +137,11 @@ const CacheDashboard: React.FC<CachePageProps> = ({
|
|||
setLastRefreshed(currentDate.toLocaleString());
|
||||
}, [accessToken]);
|
||||
|
||||
const uniqueApiKeys = Array.from(new Set(data.map((item) => item?.api_key ?? "")));
|
||||
const uniqueModels = Array.from(new Set(data.map((item) => item?.model ?? "")));
|
||||
const uniqueCallTypes = Array.from(new Set(data.map((item) => item?.call_type ?? "")));
|
||||
const uniqueApiKeys = Array.from(new Set(data.map((item) => item?.api_key ?? "")));
|
||||
const uniqueModels = Array.from(new Set(data.map((item) => item?.model ?? "")));
|
||||
const uniqueCallTypes = Array.from(new Set(data.map((item) => item?.call_type ?? "")));
|
||||
|
||||
|
||||
const updateCachingData = async (startTime: Date | undefined, endTime: Date | undefined) => {
|
||||
const updateCachingData = async (startTime: Date | undefined, endTime: Date | undefined) => {
|
||||
if (!startTime || !endTime || !accessToken) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -166,12 +149,11 @@ const CacheDashboard: React.FC<CachePageProps> = ({
|
|||
let new_cache_data = await adminGlobalCacheActivity(
|
||||
accessToken,
|
||||
formatDateWithoutTZ(startTime),
|
||||
formatDateWithoutTZ(endTime)
|
||||
)
|
||||
formatDateWithoutTZ(endTime),
|
||||
);
|
||||
|
||||
setData(new_cache_data);
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
console.log("DATA IN CACHE DASHBOARD", data);
|
||||
|
|
@ -195,7 +177,7 @@ const CacheDashboard: React.FC<CachePageProps> = ({
|
|||
{"api_key":"4f9c71cce0a2bb9a0b62ce6f0ebb3245b682702a8851d26932fa7e3b8ebfc755","call_type":"","model":"chatgpt-v-2","total_rows":1,"cache_hit_true_rows":0},
|
||||
*/
|
||||
|
||||
// What data we need for bar chat
|
||||
// What data we need for bar chat
|
||||
// ui_data = [
|
||||
// {
|
||||
// name: "Call Type",
|
||||
|
|
@ -210,226 +192,209 @@ const CacheDashboard: React.FC<CachePageProps> = ({
|
|||
let cache_hits = 0;
|
||||
let cached_tokens = 0;
|
||||
const processedData = newData.reduce((acc: uiData[], item) => {
|
||||
console.log("Processing item:", item);
|
||||
|
||||
if (!item.call_type) {
|
||||
console.log("Item has no call_type:", item);
|
||||
item.call_type = "Unknown";
|
||||
}
|
||||
console.log("Processing item:", item);
|
||||
|
||||
if (!item.call_type) {
|
||||
console.log("Item has no call_type:", item);
|
||||
item.call_type = "Unknown";
|
||||
}
|
||||
|
||||
llm_api_requests += (item.total_rows || 0) - (item.cache_hit_true_rows || 0);
|
||||
cache_hits += item.cache_hit_true_rows || 0;
|
||||
cached_tokens += item.cached_completion_tokens || 0;
|
||||
|
||||
const existingItem = acc.find(i => i.name === item.call_type);
|
||||
if (existingItem) {
|
||||
existingItem["LLM API requests"] += (item.total_rows || 0) - (item.cache_hit_true_rows || 0);
|
||||
existingItem["Cache hit"] += item.cache_hit_true_rows || 0;
|
||||
existingItem["Cached Completion Tokens"] += item.cached_completion_tokens || 0;
|
||||
existingItem["Generated Completion Tokens"] += item.generated_completion_tokens || 0;
|
||||
} else {
|
||||
acc.push({
|
||||
name: item.call_type,
|
||||
"LLM API requests": (item.total_rows || 0) - (item.cache_hit_true_rows || 0),
|
||||
"Cache hit": item.cache_hit_true_rows || 0,
|
||||
"Cached Completion Tokens": item.cached_completion_tokens || 0,
|
||||
"Generated Completion Tokens": item.generated_completion_tokens || 0
|
||||
});
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
// set header cache statistics
|
||||
llm_api_requests += (item.total_rows || 0) - (item.cache_hit_true_rows || 0);
|
||||
cache_hits += item.cache_hit_true_rows || 0;
|
||||
cached_tokens += item.cached_completion_tokens || 0;
|
||||
|
||||
const existingItem = acc.find((i) => i.name === item.call_type);
|
||||
if (existingItem) {
|
||||
existingItem["LLM API requests"] += (item.total_rows || 0) - (item.cache_hit_true_rows || 0);
|
||||
existingItem["Cache hit"] += item.cache_hit_true_rows || 0;
|
||||
existingItem["Cached Completion Tokens"] += item.cached_completion_tokens || 0;
|
||||
existingItem["Generated Completion Tokens"] += item.generated_completion_tokens || 0;
|
||||
} else {
|
||||
acc.push({
|
||||
name: item.call_type,
|
||||
"LLM API requests": (item.total_rows || 0) - (item.cache_hit_true_rows || 0),
|
||||
"Cache hit": item.cache_hit_true_rows || 0,
|
||||
"Cached Completion Tokens": item.cached_completion_tokens || 0,
|
||||
"Generated Completion Tokens": item.generated_completion_tokens || 0,
|
||||
});
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
// set header cache statistics
|
||||
setCachedResponses(valueFormatterNumbers(cache_hits));
|
||||
setCachedTokens(valueFormatterNumbers(cached_tokens));
|
||||
let allRequests = cache_hits + llm_api_requests
|
||||
let allRequests = cache_hits + llm_api_requests;
|
||||
if (allRequests > 0) {
|
||||
let cache_hit_ratio = ((cache_hits / allRequests) * 100).toFixed(2);
|
||||
setCacheHitRatio(cache_hit_ratio);
|
||||
} else {
|
||||
setCacheHitRatio("0");
|
||||
}
|
||||
|
||||
|
||||
setFilteredData(processedData);
|
||||
|
||||
console.log("PROCESSED DATA IN CACHE DASHBOARD", processedData);
|
||||
|
||||
}, [selectedApiKeys, selectedModels, dateValue, data]);
|
||||
|
||||
const handleRefreshClick = () => {
|
||||
// Update the 'lastRefreshed' state to the current date and time
|
||||
const currentDate = new Date();
|
||||
setLastRefreshed(currentDate.toLocaleString());
|
||||
};
|
||||
|
||||
const handleRefreshClick = () => {
|
||||
// Update the 'lastRefreshed' state to the current date and time
|
||||
const currentDate = new Date();
|
||||
setLastRefreshed(currentDate.toLocaleString());
|
||||
};
|
||||
|
||||
const runCachingHealthCheck = async () => {
|
||||
try {
|
||||
NotificationsManager.info("Running cache health check...");
|
||||
setHealthCheckResponse("");
|
||||
const response = await cachingHealthCheckCall(accessToken !== null ? accessToken : "");
|
||||
console.log("CACHING HEALTH CHECK RESPONSE", response);
|
||||
setHealthCheckResponse(response);
|
||||
} catch (error: any) {
|
||||
console.error("Error running health check:", error);
|
||||
let errorData;
|
||||
if (error && error.message) {
|
||||
try {
|
||||
// Parse the error message which may contain a nested error layer.
|
||||
let parsedData = JSON.parse(error.message);
|
||||
// If the parsed object is wrapped (e.g. { error: { ... } }), unwrap it.
|
||||
if (parsedData.error) {
|
||||
parsedData = parsedData.error;
|
||||
const runCachingHealthCheck = async () => {
|
||||
try {
|
||||
NotificationsManager.info("Running cache health check...");
|
||||
setHealthCheckResponse("");
|
||||
const response = await cachingHealthCheckCall(accessToken !== null ? accessToken : "");
|
||||
console.log("CACHING HEALTH CHECK RESPONSE", response);
|
||||
setHealthCheckResponse(response);
|
||||
} catch (error: any) {
|
||||
console.error("Error running health check:", error);
|
||||
let errorData;
|
||||
if (error && error.message) {
|
||||
try {
|
||||
// Parse the error message which may contain a nested error layer.
|
||||
let parsedData = JSON.parse(error.message);
|
||||
// If the parsed object is wrapped (e.g. { error: { ... } }), unwrap it.
|
||||
if (parsedData.error) {
|
||||
parsedData = parsedData.error;
|
||||
}
|
||||
errorData = parsedData;
|
||||
} catch (e) {
|
||||
errorData = { message: error.message };
|
||||
}
|
||||
errorData = parsedData;
|
||||
} catch (e) {
|
||||
errorData = { message: error.message };
|
||||
} else {
|
||||
errorData = { message: "Unknown error occurred" };
|
||||
}
|
||||
} else {
|
||||
errorData = { message: "Unknown error occurred" };
|
||||
setHealthCheckResponse({ error: errorData });
|
||||
}
|
||||
setHealthCheckResponse({ error: errorData });
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
return (
|
||||
return (
|
||||
<TabGroup className="gap-2 p-8 h-full w-full mt-2 mb-8">
|
||||
<TabList className="flex justify-between mt-2 w-full items-center">
|
||||
<div className="flex">
|
||||
<Tab>Cache Analytics</Tab>
|
||||
<Tab>
|
||||
<pre>Cache Health</pre>
|
||||
</Tab>
|
||||
</div>
|
||||
<TabList className="flex justify-between mt-2 w-full items-center">
|
||||
<div className="flex">
|
||||
<Tab>Cache Analytics</Tab>
|
||||
<Tab>
|
||||
<pre>Cache Health</pre>
|
||||
</Tab>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
{lastRefreshed && <Text>Last Refreshed: {lastRefreshed}</Text>}
|
||||
<Icon
|
||||
icon={RefreshIcon} // Modify as necessary for correct icon name
|
||||
variant="shadow"
|
||||
size="xs"
|
||||
className="self-center"
|
||||
onClick={handleRefreshClick}
|
||||
/>
|
||||
</div>
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
<TabPanel>
|
||||
<Card>
|
||||
<Grid numItems={3} className="gap-4 mt-4">
|
||||
<Col>
|
||||
<MultiSelect
|
||||
placeholder="Select API Keys"
|
||||
value={selectedApiKeys}
|
||||
onValueChange={setSelectedApiKeys}
|
||||
>
|
||||
{uniqueApiKeys.map((key) => (
|
||||
<MultiSelectItem key={key} value={key}>
|
||||
{key}
|
||||
</MultiSelectItem>
|
||||
))}
|
||||
</MultiSelect>
|
||||
</Col>
|
||||
<Col>
|
||||
<MultiSelect
|
||||
placeholder="Select Models"
|
||||
value={selectedModels}
|
||||
onValueChange={setSelectedModels}
|
||||
>
|
||||
{uniqueModels.map((model) => (
|
||||
<MultiSelectItem key={model} value={model}>
|
||||
{model}
|
||||
</MultiSelectItem>
|
||||
))}
|
||||
</MultiSelect>
|
||||
</Col>
|
||||
<Col>
|
||||
<UsageDatePicker
|
||||
value={dateValue}
|
||||
onValueChange={(value) => {
|
||||
setDateValue(value);
|
||||
updateCachingData(value.from, value.to);
|
||||
}}
|
||||
<div className="flex items-center space-x-2">
|
||||
{lastRefreshed && <Text>Last Refreshed: {lastRefreshed}</Text>}
|
||||
<Icon
|
||||
icon={RefreshIcon} // Modify as necessary for correct icon name
|
||||
variant="shadow"
|
||||
size="xs"
|
||||
className="self-center"
|
||||
onClick={handleRefreshClick}
|
||||
/>
|
||||
</Col>
|
||||
</Grid>
|
||||
|
||||
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3 mt-4">
|
||||
</div>
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
<TabPanel>
|
||||
<Card>
|
||||
<p className="text-tremor-default font-medium text-tremor-content dark:text-dark-tremor-content">
|
||||
Cache Hit Ratio
|
||||
</p>
|
||||
<div className="mt-2 flex items-baseline space-x-2.5">
|
||||
<p className="text-tremor-metric font-semibold text-tremor-content-strong dark:text-dark-tremor-content-strong">
|
||||
{cacheHitRatio}%
|
||||
</p>
|
||||
|
||||
<Grid numItems={3} className="gap-4 mt-4">
|
||||
<Col>
|
||||
<MultiSelect placeholder="Select API Keys" value={selectedApiKeys} onValueChange={setSelectedApiKeys}>
|
||||
{uniqueApiKeys.map((key) => (
|
||||
<MultiSelectItem key={key} value={key}>
|
||||
{key}
|
||||
</MultiSelectItem>
|
||||
))}
|
||||
</MultiSelect>
|
||||
</Col>
|
||||
<Col>
|
||||
<MultiSelect placeholder="Select Models" value={selectedModels} onValueChange={setSelectedModels}>
|
||||
{uniqueModels.map((model) => (
|
||||
<MultiSelectItem key={model} value={model}>
|
||||
{model}
|
||||
</MultiSelectItem>
|
||||
))}
|
||||
</MultiSelect>
|
||||
</Col>
|
||||
<Col>
|
||||
<UsageDatePicker
|
||||
value={dateValue}
|
||||
onValueChange={(value) => {
|
||||
setDateValue(value);
|
||||
updateCachingData(value.from, value.to);
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
</Grid>
|
||||
|
||||
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3 mt-4">
|
||||
<Card>
|
||||
<p className="text-tremor-default font-medium text-tremor-content dark:text-dark-tremor-content">
|
||||
Cache Hit Ratio
|
||||
</p>
|
||||
<div className="mt-2 flex items-baseline space-x-2.5">
|
||||
<p className="text-tremor-metric font-semibold text-tremor-content-strong dark:text-dark-tremor-content-strong">
|
||||
{cacheHitRatio}%
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
<Card>
|
||||
<p className="text-tremor-default font-medium text-tremor-content dark:text-dark-tremor-content">
|
||||
Cache Hits
|
||||
</p>
|
||||
<div className="mt-2 flex items-baseline space-x-2.5">
|
||||
<p className="text-tremor-metric font-semibold text-tremor-content-strong dark:text-dark-tremor-content-strong">
|
||||
{cachedResponses}
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<p className="text-tremor-default font-medium text-tremor-content dark:text-dark-tremor-content">
|
||||
Cached Tokens
|
||||
</p>
|
||||
<div className="mt-2 flex items-baseline space-x-2.5">
|
||||
<p className="text-tremor-metric font-semibold text-tremor-content-strong dark:text-dark-tremor-content-strong">
|
||||
{cachedTokens}
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</Card>
|
||||
<Card>
|
||||
<p className="text-tremor-default font-medium text-tremor-content dark:text-dark-tremor-content">
|
||||
Cache Hits
|
||||
</p>
|
||||
<div className="mt-2 flex items-baseline space-x-2.5">
|
||||
<p className="text-tremor-metric font-semibold text-tremor-content-strong dark:text-dark-tremor-content-strong">
|
||||
{cachedResponses}
|
||||
</p>
|
||||
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<p className="text-tremor-default font-medium text-tremor-content dark:text-dark-tremor-content">
|
||||
Cached Tokens
|
||||
</p>
|
||||
<div className="mt-2 flex items-baseline space-x-2.5">
|
||||
<p className="text-tremor-metric font-semibold text-tremor-content-strong dark:text-dark-tremor-content-strong">
|
||||
{cachedTokens}
|
||||
</p>
|
||||
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
</div>
|
||||
|
||||
<Subtitle className="mt-4">Cache Hits vs API Requests</Subtitle>
|
||||
<BarChart
|
||||
title="Cache Hits vs API Requests"
|
||||
data={filteredData}
|
||||
stack={true}
|
||||
index="name"
|
||||
valueFormatter={valueFormatterNumbers}
|
||||
categories={["LLM API requests", "Cache hit"]}
|
||||
colors={["sky", "teal"]}
|
||||
yAxisWidth={48}
|
||||
/>
|
||||
|
||||
<Subtitle className="mt-4">Cached Completion Tokens vs Generated Completion Tokens</Subtitle>
|
||||
<BarChart
|
||||
className="mt-6"
|
||||
data={filteredData}
|
||||
stack={true}
|
||||
index="name"
|
||||
valueFormatter={valueFormatterNumbers}
|
||||
categories={["Generated Completion Tokens", "Cached Completion Tokens"]}
|
||||
colors={["sky", "teal"]}
|
||||
yAxisWidth={48}
|
||||
/>
|
||||
|
||||
|
||||
</Card>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<CacheHealthTab
|
||||
accessToken={accessToken}
|
||||
healthCheckResponse={healthCheckResponse}
|
||||
runCachingHealthCheck={runCachingHealthCheck}
|
||||
<Subtitle className="mt-4">Cache Hits vs API Requests</Subtitle>
|
||||
<BarChart
|
||||
title="Cache Hits vs API Requests"
|
||||
data={filteredData}
|
||||
stack={true}
|
||||
index="name"
|
||||
valueFormatter={valueFormatterNumbers}
|
||||
categories={["LLM API requests", "Cache hit"]}
|
||||
colors={["sky", "teal"]}
|
||||
yAxisWidth={48}
|
||||
/>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
|
||||
<Subtitle className="mt-4">Cached Completion Tokens vs Generated Completion Tokens</Subtitle>
|
||||
<BarChart
|
||||
className="mt-6"
|
||||
data={filteredData}
|
||||
stack={true}
|
||||
index="name"
|
||||
valueFormatter={valueFormatterNumbers}
|
||||
categories={["Generated Completion Tokens", "Cached Completion Tokens"]}
|
||||
colors={["sky", "teal"]}
|
||||
yAxisWidth={48}
|
||||
/>
|
||||
</Card>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<CacheHealthTab
|
||||
accessToken={accessToken}
|
||||
healthCheckResponse={healthCheckResponse}
|
||||
runCachingHealthCheck={runCachingHealthCheck}
|
||||
/>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
);
|
||||
};
|
||||
|
||||
export default CacheDashboard;
|
||||
export default CacheDashboard;
|
||||
|
|
|
|||
|
|
@ -17,10 +17,7 @@ const deepParse = (input: any) => {
|
|||
};
|
||||
|
||||
// TableClickableErrorField component with copy-to-clipboard functionality
|
||||
const TableClickableErrorField: React.FC<{ label: string; value: string | null | undefined }> = ({
|
||||
label,
|
||||
value,
|
||||
}) => {
|
||||
const TableClickableErrorField: React.FC<{ label: string; value: string | null | undefined }> = ({ label, value }) => {
|
||||
const [isExpanded, setIsExpanded] = React.useState(false);
|
||||
const [copied, setCopied] = React.useState(false);
|
||||
const safeValue = value?.toString() || "N/A";
|
||||
|
|
@ -37,10 +34,7 @@ const TableClickableErrorField: React.FC<{ label: string; value: string | null |
|
|||
<td className="px-4 py-2 align-top" colSpan={2}>
|
||||
<div className="flex items-center justify-between group">
|
||||
<div className="flex items-center flex-1">
|
||||
<button
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
className="text-gray-400 hover:text-gray-600 mr-2"
|
||||
>
|
||||
<button onClick={() => setIsExpanded(!isExpanded)} className="text-gray-400 hover:text-gray-600 mr-2">
|
||||
{isExpanded ? "▼" : "▶"}
|
||||
</button>
|
||||
<div>
|
||||
|
|
@ -50,10 +44,7 @@ const TableClickableErrorField: React.FC<{ label: string; value: string | null |
|
|||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
className="opacity-0 group-hover:opacity-100 text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
<button onClick={handleCopy} className="opacity-0 group-hover:opacity-100 text-gray-400 hover:text-gray-600">
|
||||
<ClipboardCopyIcon className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -89,26 +80,25 @@ const HealthCheckDetails: React.FC<{ response: any }> = ({ response }) => {
|
|||
try {
|
||||
if (response?.error) {
|
||||
try {
|
||||
const errorMessage = typeof response.error.message === 'string'
|
||||
? JSON.parse(response.error.message)
|
||||
: response.error.message;
|
||||
const errorMessage =
|
||||
typeof response.error.message === "string" ? JSON.parse(response.error.message) : response.error.message;
|
||||
|
||||
errorDetails = {
|
||||
message: errorMessage?.message || 'Unknown error',
|
||||
traceback: errorMessage?.traceback || 'No traceback available',
|
||||
message: errorMessage?.message || "Unknown error",
|
||||
traceback: errorMessage?.traceback || "No traceback available",
|
||||
litellm_params: errorMessage?.litellm_cache_params || {},
|
||||
health_check_cache_params: errorMessage?.health_check_cache_params || {}
|
||||
health_check_cache_params: errorMessage?.health_check_cache_params || {},
|
||||
};
|
||||
|
||||
|
||||
parsedLitellmParams = deepParse(errorDetails.litellm_params) || {};
|
||||
parsedRedisParams = deepParse(errorDetails.health_check_cache_params) || {};
|
||||
} catch (e) {
|
||||
console.warn("Error parsing error details:", e);
|
||||
errorDetails = {
|
||||
message: String(response.error.message || 'Unknown error'),
|
||||
traceback: 'Error parsing details',
|
||||
message: String(response.error.message || "Unknown error"),
|
||||
traceback: "Error parsing details",
|
||||
litellm_params: {},
|
||||
health_check_cache_params: {}
|
||||
health_check_cache_params: {},
|
||||
};
|
||||
}
|
||||
} else {
|
||||
|
|
@ -124,29 +114,33 @@ const HealthCheckDetails: React.FC<{ response: any }> = ({ response }) => {
|
|||
|
||||
// Safely extract Redis details with fallbacks
|
||||
const redisDetails: RedisDetails = {
|
||||
redis_host: parsedRedisParams?.redis_client?.connection_pool?.connection_kwargs?.host ||
|
||||
parsedRedisParams?.redis_async_client?.connection_pool?.connection_kwargs?.host ||
|
||||
parsedRedisParams?.connection_kwargs?.host ||
|
||||
parsedRedisParams?.host ||
|
||||
"N/A",
|
||||
|
||||
redis_port: parsedRedisParams?.redis_client?.connection_pool?.connection_kwargs?.port ||
|
||||
parsedRedisParams?.redis_async_client?.connection_pool?.connection_kwargs?.port ||
|
||||
parsedRedisParams?.connection_kwargs?.port ||
|
||||
parsedRedisParams?.port ||
|
||||
"N/A",
|
||||
|
||||
redis_host:
|
||||
parsedRedisParams?.redis_client?.connection_pool?.connection_kwargs?.host ||
|
||||
parsedRedisParams?.redis_async_client?.connection_pool?.connection_kwargs?.host ||
|
||||
parsedRedisParams?.connection_kwargs?.host ||
|
||||
parsedRedisParams?.host ||
|
||||
"N/A",
|
||||
|
||||
redis_port:
|
||||
parsedRedisParams?.redis_client?.connection_pool?.connection_kwargs?.port ||
|
||||
parsedRedisParams?.redis_async_client?.connection_pool?.connection_kwargs?.port ||
|
||||
parsedRedisParams?.connection_kwargs?.port ||
|
||||
parsedRedisParams?.port ||
|
||||
"N/A",
|
||||
|
||||
redis_version: parsedRedisParams?.redis_version || "N/A",
|
||||
|
||||
|
||||
startup_nodes: (() => {
|
||||
try {
|
||||
if (parsedRedisParams?.redis_kwargs?.startup_nodes) {
|
||||
return JSON.stringify(parsedRedisParams.redis_kwargs.startup_nodes);
|
||||
}
|
||||
const host = parsedRedisParams?.redis_client?.connection_pool?.connection_kwargs?.host ||
|
||||
parsedRedisParams?.redis_async_client?.connection_pool?.connection_kwargs?.host;
|
||||
const port = parsedRedisParams?.redis_client?.connection_pool?.connection_kwargs?.port ||
|
||||
parsedRedisParams?.redis_async_client?.connection_pool?.connection_kwargs?.port;
|
||||
const host =
|
||||
parsedRedisParams?.redis_client?.connection_pool?.connection_kwargs?.host ||
|
||||
parsedRedisParams?.redis_async_client?.connection_pool?.connection_kwargs?.host;
|
||||
const port =
|
||||
parsedRedisParams?.redis_client?.connection_pool?.connection_kwargs?.port ||
|
||||
parsedRedisParams?.redis_async_client?.connection_pool?.connection_kwargs?.port;
|
||||
return host && port ? JSON.stringify([{ host, port }]) : "N/A";
|
||||
} catch (e) {
|
||||
return "N/A";
|
||||
|
|
@ -168,12 +162,14 @@ const HealthCheckDetails: React.FC<{ response: any }> = ({ response }) => {
|
|||
<TabPanel className="p-4">
|
||||
<div>
|
||||
<div className="flex items-center mb-6">
|
||||
{(response?.status === "healthy") ? (
|
||||
{response?.status === "healthy" ? (
|
||||
<CheckCircleIcon className="h-5 w-5 text-green-500 mr-2" />
|
||||
) : (
|
||||
<XCircleIcon className="h-5 w-5 text-red-500 mr-2" />
|
||||
)}
|
||||
<Text className={`text-sm font-medium ${response?.status === "healthy" ? "text-green-500" : "text-red-500"}`}>
|
||||
<Text
|
||||
className={`text-sm font-medium ${response?.status === "healthy" ? "text-green-500" : "text-red-500"}`}
|
||||
>
|
||||
Cache Status: {response?.status || "unhealthy"}
|
||||
</Text>
|
||||
</div>
|
||||
|
|
@ -183,61 +179,43 @@ const HealthCheckDetails: React.FC<{ response: any }> = ({ response }) => {
|
|||
{/* Show error message if present */}
|
||||
{errorDetails && (
|
||||
<>
|
||||
<tr><td colSpan={2} className="pt-4 pb-2 font-semibold text-red-600">Error Details</td></tr>
|
||||
<TableClickableErrorField
|
||||
label="Error Message"
|
||||
value={errorDetails.message}
|
||||
/>
|
||||
<TableClickableErrorField
|
||||
label="Traceback"
|
||||
value={errorDetails.traceback}
|
||||
/>
|
||||
<tr>
|
||||
<td colSpan={2} className="pt-4 pb-2 font-semibold text-red-600">
|
||||
Error Details
|
||||
</td>
|
||||
</tr>
|
||||
<TableClickableErrorField label="Error Message" value={errorDetails.message} />
|
||||
<TableClickableErrorField label="Traceback" value={errorDetails.traceback} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Always show cache details, regardless of error state */}
|
||||
<tr><td colSpan={2} className="pt-4 pb-2 font-semibold">Cache Details</td></tr>
|
||||
<TableClickableErrorField
|
||||
label="Cache Configuration"
|
||||
value={String(parsedLitellmParams?.type)}
|
||||
/>
|
||||
<TableClickableErrorField
|
||||
label="Ping Response"
|
||||
value={String(response.ping_response)}
|
||||
/>
|
||||
<TableClickableErrorField
|
||||
label="Set Cache Response"
|
||||
value={response.set_cache_response || "N/A"}
|
||||
/>
|
||||
<tr>
|
||||
<td colSpan={2} className="pt-4 pb-2 font-semibold">
|
||||
Cache Details
|
||||
</td>
|
||||
</tr>
|
||||
<TableClickableErrorField label="Cache Configuration" value={String(parsedLitellmParams?.type)} />
|
||||
<TableClickableErrorField label="Ping Response" value={String(response.ping_response)} />
|
||||
<TableClickableErrorField label="Set Cache Response" value={response.set_cache_response || "N/A"} />
|
||||
<TableClickableErrorField
|
||||
label="litellm_settings.cache_params"
|
||||
value={JSON.stringify(parsedLitellmParams, null, 2)}
|
||||
/>
|
||||
|
||||
|
||||
{/* Redis Details Section */}
|
||||
{parsedLitellmParams?.type === "redis" && (
|
||||
<>
|
||||
<tr><td colSpan={2} className="pt-4 pb-2 font-semibold">Redis Details</td></tr>
|
||||
<TableClickableErrorField
|
||||
label="Redis Host"
|
||||
value={redisDetails.redis_host || "N/A"}
|
||||
/>
|
||||
<TableClickableErrorField
|
||||
label="Redis Port"
|
||||
value={redisDetails.redis_port || "N/A"}
|
||||
/>
|
||||
<TableClickableErrorField
|
||||
label="Redis Version"
|
||||
value={redisDetails.redis_version || "N/A"}
|
||||
/>
|
||||
<TableClickableErrorField
|
||||
label="Startup Nodes"
|
||||
value={redisDetails.startup_nodes || "N/A"}
|
||||
/>
|
||||
<TableClickableErrorField
|
||||
label="Namespace"
|
||||
value={redisDetails.namespace || "N/A"}
|
||||
/>
|
||||
<tr>
|
||||
<td colSpan={2} className="pt-4 pb-2 font-semibold">
|
||||
Redis Details
|
||||
</td>
|
||||
</tr>
|
||||
<TableClickableErrorField label="Redis Host" value={redisDetails.redis_host || "N/A"} />
|
||||
<TableClickableErrorField label="Redis Port" value={redisDetails.redis_port || "N/A"} />
|
||||
<TableClickableErrorField label="Redis Version" value={redisDetails.redis_version || "N/A"} />
|
||||
<TableClickableErrorField label="Startup Nodes" value={redisDetails.startup_nodes || "N/A"} />
|
||||
<TableClickableErrorField label="Namespace" value={redisDetails.namespace || "N/A"} />
|
||||
</>
|
||||
)}
|
||||
</tbody>
|
||||
|
|
@ -253,19 +231,21 @@ const HealthCheckDetails: React.FC<{ response: any }> = ({ response }) => {
|
|||
const data = {
|
||||
...response,
|
||||
litellm_cache_params: parsedLitellmParams,
|
||||
health_check_cache_params: parsedRedisParams
|
||||
health_check_cache_params: parsedRedisParams,
|
||||
};
|
||||
// First parse any string JSON values
|
||||
const prettyData = JSON.parse(JSON.stringify(data, (key, value) => {
|
||||
if (typeof value === 'string') {
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
} catch {
|
||||
return value;
|
||||
const prettyData = JSON.parse(
|
||||
JSON.stringify(data, (key, value) => {
|
||||
if (typeof value === "string") {
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
} catch {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}));
|
||||
return value;
|
||||
}),
|
||||
);
|
||||
// Then stringify with proper formatting
|
||||
return JSON.stringify(prettyData, null, 2);
|
||||
} catch (e) {
|
||||
|
|
@ -281,7 +261,7 @@ const HealthCheckDetails: React.FC<{ response: any }> = ({ response }) => {
|
|||
);
|
||||
};
|
||||
|
||||
export const CacheHealthTab: React.FC<{
|
||||
export const CacheHealthTab: React.FC<{
|
||||
accessToken: string | null;
|
||||
healthCheckResponse: any;
|
||||
runCachingHealthCheck: () => void;
|
||||
|
|
@ -302,7 +282,7 @@ export const CacheHealthTab: React.FC<{
|
|||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Button
|
||||
<Button
|
||||
onClick={handleHealthCheck}
|
||||
disabled={isLoading}
|
||||
className="bg-indigo-600 hover:bg-indigo-700 disabled:bg-indigo-400 text-white text-sm px-4 py-2 rounded-md"
|
||||
|
|
@ -312,9 +292,7 @@ export const CacheHealthTab: React.FC<{
|
|||
<ResponseTimeIndicator responseTimeMs={localResponseTimeMs} />
|
||||
</div>
|
||||
|
||||
{healthCheckResponse && (
|
||||
<HealthCheckDetails response={healthCheckResponse} />
|
||||
)}
|
||||
{healthCheckResponse && <HealthCheckDetails response={healthCheckResponse} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -24,24 +24,24 @@ export const callback_map: Record<string, string> = {
|
|||
OTel: "otel",
|
||||
S3: "s3",
|
||||
Arize: "arize",
|
||||
}
|
||||
};
|
||||
|
||||
// Reverse mapping from internal values to display names
|
||||
export const reverse_callback_map: Record<string, string> = Object.fromEntries(
|
||||
Object.entries(callback_map).map(([key, value]) => [value, key])
|
||||
Object.entries(callback_map).map(([key, value]) => [value, key]),
|
||||
);
|
||||
|
||||
// Utility function to convert internal callback values to display names
|
||||
export const mapInternalToDisplayNames = (internalValues: string[]): string[] => {
|
||||
return internalValues.map(value => reverse_callback_map[value] || value);
|
||||
return internalValues.map((value) => reverse_callback_map[value] || value);
|
||||
};
|
||||
|
||||
// Utility function to convert display names to internal callback values
|
||||
export const mapDisplayToInternalNames = (displayValues: string[]): string[] => {
|
||||
return displayValues.map(value => callback_map[value] || value);
|
||||
return displayValues.map((value) => callback_map[value] || value);
|
||||
};
|
||||
|
||||
const asset_logos_folder = '/ui/assets/logos/';
|
||||
const asset_logos_folder = "/ui/assets/logos/";
|
||||
|
||||
interface CallbackInfo {
|
||||
logo: string;
|
||||
|
|
@ -55,83 +55,82 @@ export const callbackInfo: Record<string, CallbackInfo> = {
|
|||
logo: `${asset_logos_folder}langfuse.png`,
|
||||
supports_key_team_logging: true,
|
||||
dynamic_params: {
|
||||
"langfuse_public_key": "text",
|
||||
"langfuse_secret_key": "password",
|
||||
"langfuse_host": "text"
|
||||
langfuse_public_key: "text",
|
||||
langfuse_secret_key: "password",
|
||||
langfuse_host: "text",
|
||||
},
|
||||
description: "Langfuse v2 Logging Integration"
|
||||
description: "Langfuse v2 Logging Integration",
|
||||
},
|
||||
[Callbacks.LangfuseOtel]: {
|
||||
logo: `${asset_logos_folder}langfuse.png`,
|
||||
supports_key_team_logging: true,
|
||||
dynamic_params: {
|
||||
langfuse_public_key: "text",
|
||||
langfuse_secret_key: "password",
|
||||
langfuse_host: "text",
|
||||
},
|
||||
[Callbacks.LangfuseOtel]: {
|
||||
logo: `${asset_logos_folder}langfuse.png`,
|
||||
supports_key_team_logging: true,
|
||||
dynamic_params: {
|
||||
"langfuse_public_key": "text",
|
||||
"langfuse_secret_key": "password",
|
||||
"langfuse_host": "text"
|
||||
},
|
||||
description: "Langfuse v3 OTEL Logging Integration"
|
||||
description: "Langfuse v3 OTEL Logging Integration",
|
||||
},
|
||||
[Callbacks.Arize]: {
|
||||
logo: `${asset_logos_folder}arize.png`,
|
||||
supports_key_team_logging: true,
|
||||
dynamic_params: {
|
||||
arize_api_key: "password",
|
||||
arize_space_id: "text",
|
||||
},
|
||||
[Callbacks.Arize]: {
|
||||
logo: `${asset_logos_folder}arize.png`,
|
||||
supports_key_team_logging: true,
|
||||
dynamic_params: {
|
||||
"arize_api_key": "password",
|
||||
"arize_space_id": "text",
|
||||
},
|
||||
description: "Arize Logging Integration"
|
||||
description: "Arize Logging Integration",
|
||||
},
|
||||
[Callbacks.LangSmith]: {
|
||||
logo: `${asset_logos_folder}langsmith.png`,
|
||||
supports_key_team_logging: true,
|
||||
dynamic_params: {
|
||||
langsmith_api_key: "password",
|
||||
langsmith_project: "text",
|
||||
langsmith_base_url: "text",
|
||||
langsmith_sampling_rate: "number",
|
||||
},
|
||||
[Callbacks.LangSmith]: {
|
||||
logo: `${asset_logos_folder}langsmith.png`,
|
||||
supports_key_team_logging: true,
|
||||
dynamic_params: {
|
||||
"langsmith_api_key": "password",
|
||||
"langsmith_project": "text",
|
||||
"langsmith_base_url": "text",
|
||||
"langsmith_sampling_rate": "number"
|
||||
},
|
||||
description: "Langsmith Logging Integration"
|
||||
},
|
||||
[Callbacks.Braintrust]: {
|
||||
logo: `${asset_logos_folder}braintrust.png`,
|
||||
supports_key_team_logging: false,
|
||||
dynamic_params: {},
|
||||
description: "Braintrust Logging Integration"
|
||||
},
|
||||
[Callbacks.CustomCallbackAPI]: {
|
||||
logo: `${asset_logos_folder}custom.svg`,
|
||||
supports_key_team_logging: true,
|
||||
dynamic_params: {},
|
||||
description: "Custom Callback API Logging Integration"
|
||||
},
|
||||
[Callbacks.Datadog]: {
|
||||
logo: `${asset_logos_folder}datadog.png`,
|
||||
supports_key_team_logging: false,
|
||||
dynamic_params: {},
|
||||
description: "Datadog Logging Integration"
|
||||
},
|
||||
[Callbacks.Lago]: {
|
||||
logo: `${asset_logos_folder}lago.svg`,
|
||||
supports_key_team_logging: false,
|
||||
dynamic_params: {},
|
||||
description: "Lago Billing Logging Integration"
|
||||
},
|
||||
[Callbacks.OpenMeter]: {
|
||||
logo: `${asset_logos_folder}openmeter.png`,
|
||||
supports_key_team_logging: false,
|
||||
dynamic_params: {},
|
||||
description: "OpenMeter Logging Integration"
|
||||
},
|
||||
[Callbacks.OTel]: {
|
||||
logo: `${asset_logos_folder}otel.png`,
|
||||
supports_key_team_logging: false,
|
||||
dynamic_params: {},
|
||||
description: "OpenTelemetry Logging Integration"
|
||||
},
|
||||
[Callbacks.S3]: {
|
||||
logo: `${asset_logos_folder}aws.svg`,
|
||||
supports_key_team_logging: false,
|
||||
dynamic_params: {},
|
||||
description: "S3 Bucket (AWS) Logging Integration"
|
||||
}
|
||||
description: "Langsmith Logging Integration",
|
||||
},
|
||||
[Callbacks.Braintrust]: {
|
||||
logo: `${asset_logos_folder}braintrust.png`,
|
||||
supports_key_team_logging: false,
|
||||
dynamic_params: {},
|
||||
description: "Braintrust Logging Integration",
|
||||
},
|
||||
[Callbacks.CustomCallbackAPI]: {
|
||||
logo: `${asset_logos_folder}custom.svg`,
|
||||
supports_key_team_logging: true,
|
||||
dynamic_params: {},
|
||||
description: "Custom Callback API Logging Integration",
|
||||
},
|
||||
[Callbacks.Datadog]: {
|
||||
logo: `${asset_logos_folder}datadog.png`,
|
||||
supports_key_team_logging: false,
|
||||
dynamic_params: {},
|
||||
description: "Datadog Logging Integration",
|
||||
},
|
||||
[Callbacks.Lago]: {
|
||||
logo: `${asset_logos_folder}lago.svg`,
|
||||
supports_key_team_logging: false,
|
||||
dynamic_params: {},
|
||||
description: "Lago Billing Logging Integration",
|
||||
},
|
||||
[Callbacks.OpenMeter]: {
|
||||
logo: `${asset_logos_folder}openmeter.png`,
|
||||
supports_key_team_logging: false,
|
||||
dynamic_params: {},
|
||||
description: "OpenMeter Logging Integration",
|
||||
},
|
||||
[Callbacks.OTel]: {
|
||||
logo: `${asset_logos_folder}otel.png`,
|
||||
supports_key_team_logging: false,
|
||||
dynamic_params: {},
|
||||
description: "OpenTelemetry Logging Integration",
|
||||
},
|
||||
[Callbacks.S3]: {
|
||||
logo: `${asset_logos_folder}aws.svg`,
|
||||
supports_key_team_logging: false,
|
||||
dynamic_params: {},
|
||||
description: "S3 Bucket (AWS) Logging Integration",
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -18,18 +18,18 @@ const ChatImageRenderer: React.FC<ChatImageRendererProps> = ({ message }) => {
|
|||
<div className="mb-2">
|
||||
{isPdf ? (
|
||||
<div className="w-64 h-32 rounded-md border border-gray-200 bg-red-50 flex items-center justify-center">
|
||||
<FilePdfOutlined style={{ fontSize: '48px', color: '#dc2626' }} />
|
||||
<FilePdfOutlined style={{ fontSize: "48px", color: "#dc2626" }} />
|
||||
</div>
|
||||
) : (
|
||||
<img
|
||||
src={message.imagePreviewUrl}
|
||||
alt="User uploaded image"
|
||||
className="max-w-64 rounded-md border border-gray-200 shadow-sm"
|
||||
style={{ maxHeight: '200px' }}
|
||||
<img
|
||||
src={message.imagePreviewUrl}
|
||||
alt="User uploaded image"
|
||||
className="max-w-64 rounded-md border border-gray-200 shadow-sm"
|
||||
style={{ maxHeight: "200px" }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatImageRenderer;
|
||||
export default ChatImageRenderer;
|
||||
|
|
|
|||
|
|
@ -26,14 +26,14 @@ const ChatImageUpload: React.FC<ChatImageUploadProps> = ({
|
|||
accept="image/*,.pdf"
|
||||
showUploadList={false}
|
||||
className="inline-block"
|
||||
style={{ padding: 0, border: 'none', background: 'none' }}
|
||||
style={{ padding: 0, border: "none", background: "none" }}
|
||||
>
|
||||
<Tooltip title="Attach image or PDF">
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center justify-center w-8 h-8 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-md transition-colors"
|
||||
>
|
||||
<PaperClipOutlined style={{ fontSize: '16px' }} />
|
||||
<PaperClipOutlined style={{ fontSize: "16px" }} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</Dragger>
|
||||
|
|
@ -42,4 +42,4 @@ const ChatImageUpload: React.FC<ChatImageUploadProps> = ({
|
|||
);
|
||||
};
|
||||
|
||||
export default ChatImageUpload;
|
||||
export default ChatImageUpload;
|
||||
|
|
|
|||
|
|
@ -24,10 +24,10 @@ export const convertImageToBase64 = (file: File): Promise<string> => {
|
|||
|
||||
export const createChatMultimodalMessage = async (
|
||||
inputMessage: string,
|
||||
file: File
|
||||
file: File,
|
||||
): Promise<{ role: string; content: ChatMultimodalContent[] }> => {
|
||||
const base64DataUri = await convertImageToBase64(file);
|
||||
|
||||
|
||||
return {
|
||||
role: "user",
|
||||
content: [
|
||||
|
|
@ -46,30 +46,30 @@ export const createChatDisplayMessage = (
|
|||
inputMessage: string,
|
||||
hasFile: boolean,
|
||||
filePreviewUrl?: string,
|
||||
fileName?: string
|
||||
fileName?: string,
|
||||
): MessageType => {
|
||||
let attachmentText = "";
|
||||
if (hasFile && fileName) {
|
||||
attachmentText = fileName.toLowerCase().endsWith('.pdf') ? "[PDF attached]" : "[Image attached]";
|
||||
attachmentText = fileName.toLowerCase().endsWith(".pdf") ? "[PDF attached]" : "[Image attached]";
|
||||
}
|
||||
|
||||
const displayMessage: MessageType = {
|
||||
role: "user",
|
||||
content: hasFile ? `${inputMessage} ${attachmentText}` : inputMessage
|
||||
|
||||
const displayMessage: MessageType = {
|
||||
role: "user",
|
||||
content: hasFile ? `${inputMessage} ${attachmentText}` : inputMessage,
|
||||
};
|
||||
|
||||
|
||||
if (hasFile && filePreviewUrl) {
|
||||
displayMessage.imagePreviewUrl = filePreviewUrl;
|
||||
}
|
||||
|
||||
|
||||
return displayMessage;
|
||||
};
|
||||
|
||||
export const shouldShowChatAttachedImage = (message: MessageType): boolean => {
|
||||
return (
|
||||
message.role === "user" &&
|
||||
typeof message.content === "string" &&
|
||||
(message.content.includes("[Image attached]") || message.content.includes("[PDF attached]")) &&
|
||||
message.role === "user" &&
|
||||
typeof message.content === "string" &&
|
||||
(message.content.includes("[Image attached]") || message.content.includes("[PDF attached]")) &&
|
||||
!!message.imagePreviewUrl
|
||||
);
|
||||
};
|
||||
};
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,95 +1,97 @@
|
|||
import { MessageType } from "./types";
|
||||
import { EndpointType } from "./mode_endpoint_mapping";
|
||||
|
||||
|
||||
interface CodeGenMetadata {
|
||||
tags?: string[];
|
||||
vector_stores?: string[];
|
||||
guardrails?: string[];
|
||||
tags?: string[];
|
||||
vector_stores?: string[];
|
||||
guardrails?: string[];
|
||||
}
|
||||
|
||||
interface GenerateCodeParams {
|
||||
apiKeySource: 'session' | 'custom';
|
||||
accessToken: string | null;
|
||||
apiKey: string;
|
||||
inputMessage: string;
|
||||
chatHistory: MessageType[];
|
||||
selectedTags: string[];
|
||||
selectedVectorStores: string[];
|
||||
selectedGuardrails: string[];
|
||||
selectedMCPTools: string[];
|
||||
endpointType: string;
|
||||
selectedModel: string | undefined;
|
||||
selectedSdk: 'openai' | 'azure';
|
||||
apiKeySource: "session" | "custom";
|
||||
accessToken: string | null;
|
||||
apiKey: string;
|
||||
inputMessage: string;
|
||||
chatHistory: MessageType[];
|
||||
selectedTags: string[];
|
||||
selectedVectorStores: string[];
|
||||
selectedGuardrails: string[];
|
||||
selectedMCPTools: string[];
|
||||
endpointType: string;
|
||||
selectedModel: string | undefined;
|
||||
selectedSdk: "openai" | "azure";
|
||||
}
|
||||
|
||||
export const generateCodeSnippet = (params: GenerateCodeParams): string => {
|
||||
const {
|
||||
apiKeySource,
|
||||
accessToken,
|
||||
apiKey,
|
||||
inputMessage,
|
||||
chatHistory,
|
||||
selectedTags,
|
||||
selectedVectorStores,
|
||||
selectedGuardrails,
|
||||
selectedMCPTools,
|
||||
endpointType,
|
||||
selectedModel,
|
||||
selectedSdk,
|
||||
} = params;
|
||||
const effectiveApiKey = apiKeySource === 'session' ? accessToken : apiKey;
|
||||
const apiBase = window.location.origin;
|
||||
const {
|
||||
apiKeySource,
|
||||
accessToken,
|
||||
apiKey,
|
||||
inputMessage,
|
||||
chatHistory,
|
||||
selectedTags,
|
||||
selectedVectorStores,
|
||||
selectedGuardrails,
|
||||
selectedMCPTools,
|
||||
endpointType,
|
||||
selectedModel,
|
||||
selectedSdk,
|
||||
} = params;
|
||||
const effectiveApiKey = apiKeySource === "session" ? accessToken : apiKey;
|
||||
const apiBase = window.location.origin;
|
||||
|
||||
// Always get the input message early on, regardless of what happens later
|
||||
const userPrompt = inputMessage || "Your prompt here"; // Fallback if inputMessage is empty
|
||||
// Always get the input message early on, regardless of what happens later
|
||||
const userPrompt = inputMessage || "Your prompt here"; // Fallback if inputMessage is empty
|
||||
|
||||
// Safely escape the prompt to prevent issues with quotes
|
||||
const safePrompt = userPrompt.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n');
|
||||
// Safely escape the prompt to prevent issues with quotes
|
||||
const safePrompt = userPrompt.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n");
|
||||
|
||||
const messages = chatHistory
|
||||
.filter(msg => !msg.isImage)
|
||||
.map(({ role, content }) => ({ role, content }));
|
||||
const messages = chatHistory.filter((msg) => !msg.isImage).map(({ role, content }) => ({ role, content }));
|
||||
|
||||
const metadata: CodeGenMetadata = {};
|
||||
if (selectedTags.length > 0) metadata.tags = selectedTags;
|
||||
if (selectedVectorStores.length > 0) metadata.vector_stores = selectedVectorStores;
|
||||
if (selectedGuardrails.length > 0) metadata.guardrails = selectedGuardrails;
|
||||
const metadata: CodeGenMetadata = {};
|
||||
if (selectedTags.length > 0) metadata.tags = selectedTags;
|
||||
if (selectedVectorStores.length > 0) metadata.vector_stores = selectedVectorStores;
|
||||
if (selectedGuardrails.length > 0) metadata.guardrails = selectedGuardrails;
|
||||
|
||||
const modelNameForCode = selectedModel || 'your-model-name';
|
||||
const modelNameForCode = selectedModel || "your-model-name";
|
||||
|
||||
const clientInitialization = selectedSdk === 'azure'
|
||||
? `import openai
|
||||
const clientInitialization =
|
||||
selectedSdk === "azure"
|
||||
? `import openai
|
||||
|
||||
client = openai.AzureOpenAI(
|
||||
api_key="${effectiveApiKey || 'YOUR_LITELLM_API_KEY'}",
|
||||
api_key="${effectiveApiKey || "YOUR_LITELLM_API_KEY"}",
|
||||
azure_endpoint="${apiBase}",
|
||||
api_version="2024-02-01"
|
||||
)`
|
||||
: `import openai
|
||||
: `import openai
|
||||
|
||||
client = openai.OpenAI(
|
||||
api_key="${effectiveApiKey || 'YOUR_LITELLM_API_KEY'}",
|
||||
api_key="${effectiveApiKey || "YOUR_LITELLM_API_KEY"}",
|
||||
base_url="${apiBase}"
|
||||
)`;
|
||||
|
||||
let endpointSpecificCode;
|
||||
switch (endpointType) {
|
||||
case EndpointType.CHAT: {
|
||||
const metadataIsNotEmpty = Object.keys(metadata).length > 0;
|
||||
let extraBodyCode = '';
|
||||
if (metadataIsNotEmpty) {
|
||||
const extraBodyObject = { metadata };
|
||||
const extraBodyString = JSON.stringify(extraBodyObject, null, 2);
|
||||
const indentedExtraBodyString = extraBodyString.split('\n').map(line => ' '.repeat(4) + line).join('\n').trim();
|
||||
let endpointSpecificCode;
|
||||
switch (endpointType) {
|
||||
case EndpointType.CHAT: {
|
||||
const metadataIsNotEmpty = Object.keys(metadata).length > 0;
|
||||
let extraBodyCode = "";
|
||||
if (metadataIsNotEmpty) {
|
||||
const extraBodyObject = { metadata };
|
||||
const extraBodyString = JSON.stringify(extraBodyObject, null, 2);
|
||||
const indentedExtraBodyString = extraBodyString
|
||||
.split("\n")
|
||||
.map((line) => " ".repeat(4) + line)
|
||||
.join("\n")
|
||||
.trim();
|
||||
|
||||
extraBodyCode = `,\n extra_body=${indentedExtraBodyString}`;
|
||||
}
|
||||
extraBodyCode = `,\n extra_body=${indentedExtraBodyString}`;
|
||||
}
|
||||
|
||||
// Create example for chat completions with optional image support
|
||||
const messagesExample = messages.length > 0 ? messages : [{ role: "user", content: userPrompt }];
|
||||
// Create example for chat completions with optional image support
|
||||
const messagesExample = messages.length > 0 ? messages : [{ role: "user", content: userPrompt }];
|
||||
|
||||
endpointSpecificCode = `
|
||||
endpointSpecificCode = `
|
||||
import base64
|
||||
|
||||
# Helper function to encode images to base64
|
||||
|
|
@ -129,23 +131,27 @@ print(response)
|
|||
# )
|
||||
# print(response_with_file)
|
||||
`;
|
||||
break;
|
||||
}
|
||||
case EndpointType.RESPONSES: {
|
||||
const metadataIsNotEmpty = Object.keys(metadata).length > 0;
|
||||
let extraBodyCode = '';
|
||||
if (metadataIsNotEmpty) {
|
||||
const extraBodyObject = { metadata };
|
||||
const extraBodyString = JSON.stringify(extraBodyObject, null, 2);
|
||||
const indentedExtraBodyString = extraBodyString.split('\n').map(line => ' '.repeat(4) + line).join('\n').trim();
|
||||
break;
|
||||
}
|
||||
case EndpointType.RESPONSES: {
|
||||
const metadataIsNotEmpty = Object.keys(metadata).length > 0;
|
||||
let extraBodyCode = "";
|
||||
if (metadataIsNotEmpty) {
|
||||
const extraBodyObject = { metadata };
|
||||
const extraBodyString = JSON.stringify(extraBodyObject, null, 2);
|
||||
const indentedExtraBodyString = extraBodyString
|
||||
.split("\n")
|
||||
.map((line) => " ".repeat(4) + line)
|
||||
.join("\n")
|
||||
.trim();
|
||||
|
||||
extraBodyCode = `,\n extra_body=${indentedExtraBodyString}`;
|
||||
}
|
||||
extraBodyCode = `,\n extra_body=${indentedExtraBodyString}`;
|
||||
}
|
||||
|
||||
// Create example for responses API with optional image support
|
||||
const inputExample = messages.length > 0 ? messages : [{ role: "user", content: userPrompt }];
|
||||
|
||||
endpointSpecificCode = `
|
||||
// Create example for responses API with optional image support
|
||||
const inputExample = messages.length > 0 ? messages : [{ role: "user", content: userPrompt }];
|
||||
|
||||
endpointSpecificCode = `
|
||||
import base64
|
||||
|
||||
# Helper function to encode images to base64
|
||||
|
|
@ -180,11 +186,11 @@ print(response.output_text)
|
|||
# )
|
||||
# print(response_with_file.output_text)
|
||||
`;
|
||||
break;
|
||||
}
|
||||
case EndpointType.IMAGE:
|
||||
if (selectedSdk === 'azure') {
|
||||
endpointSpecificCode = `
|
||||
break;
|
||||
}
|
||||
case EndpointType.IMAGE:
|
||||
if (selectedSdk === "azure") {
|
||||
endpointSpecificCode = `
|
||||
# NOTE: The Azure SDK does not have a direct equivalent to the multi-modal 'responses.create' method shown for OpenAI.
|
||||
# This snippet uses 'client.images.generate' and will create a new image based on your prompt.
|
||||
# It does not use the uploaded image, as 'client.images.generate' does not support image inputs in this context.
|
||||
|
|
@ -232,8 +238,8 @@ except Exception as e:
|
|||
print(f"An error occurred: {e}")
|
||||
print("Full response:", json_response)
|
||||
`;
|
||||
} else {
|
||||
endpointSpecificCode = `
|
||||
} else {
|
||||
endpointSpecificCode = `
|
||||
import base64
|
||||
import os
|
||||
import time
|
||||
|
|
@ -318,12 +324,12 @@ else:
|
|||
print("Full response for debugging:")
|
||||
print(response)
|
||||
`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case EndpointType.IMAGE_EDITS:
|
||||
if (selectedSdk === 'azure') {
|
||||
endpointSpecificCode = `
|
||||
case EndpointType.IMAGE_EDITS:
|
||||
if (selectedSdk === "azure") {
|
||||
endpointSpecificCode = `
|
||||
import base64
|
||||
import os
|
||||
import time
|
||||
|
|
@ -402,8 +408,8 @@ else:
|
|||
print("Full response for debugging:")
|
||||
print(response)
|
||||
`;
|
||||
} else {
|
||||
endpointSpecificCode = `
|
||||
} else {
|
||||
endpointSpecificCode = `
|
||||
import base64
|
||||
import os
|
||||
import time
|
||||
|
|
@ -485,13 +491,13 @@ else:
|
|||
print("Full response for debugging:")
|
||||
print(response)
|
||||
`;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
endpointSpecificCode = "\n# Code generation for this endpoint is not implemented yet.";
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
endpointSpecificCode = "\n# Code generation for this endpoint is not implemented yet.";
|
||||
}
|
||||
|
||||
const finalCode = `${clientInitialization}\n${endpointSpecificCode}`;
|
||||
const finalCode = `${clientInitialization}\n${endpointSpecificCode}`;
|
||||
|
||||
return finalCode;
|
||||
};
|
||||
return finalCode;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -12,18 +12,14 @@ interface EndpointSelectorProps {
|
|||
/**
|
||||
* A reusable component for selecting API endpoints
|
||||
*/
|
||||
const EndpointSelector: React.FC<EndpointSelectorProps> = ({
|
||||
endpointType,
|
||||
onEndpointChange,
|
||||
className,
|
||||
}) => {
|
||||
const EndpointSelector: React.FC<EndpointSelectorProps> = ({ endpointType, onEndpointChange, className }) => {
|
||||
// Map endpoint types to their display labels
|
||||
const endpointOptions = [
|
||||
{ value: EndpointType.CHAT, label: '/v1/chat/completions' },
|
||||
{ value: EndpointType.RESPONSES, label: '/v1/responses' },
|
||||
{ value: EndpointType.ANTHROPIC_MESSAGES, label: '/v1/messages' },
|
||||
{ value: EndpointType.IMAGE, label: '/v1/images/generations' },
|
||||
{ value: EndpointType.IMAGE_EDITS, label: '/v1/images/edits' },
|
||||
{ value: EndpointType.CHAT, label: "/v1/chat/completions" },
|
||||
{ value: EndpointType.RESPONSES, label: "/v1/responses" },
|
||||
{ value: EndpointType.ANTHROPIC_MESSAGES, label: "/v1/messages" },
|
||||
{ value: EndpointType.IMAGE, label: "/v1/images/generations" },
|
||||
{ value: EndpointType.IMAGE_EDITS, label: "/v1/images/edits" },
|
||||
];
|
||||
|
||||
return (
|
||||
|
|
@ -41,4 +37,4 @@ const EndpointSelector: React.FC<EndpointSelectorProps> = ({
|
|||
);
|
||||
};
|
||||
|
||||
export default EndpointSelector;
|
||||
export default EndpointSelector;
|
||||
|
|
|
|||
|
|
@ -3,25 +3,20 @@ import { ModelMode, EndpointType, getEndpointType } from "./mode_endpoint_mappin
|
|||
|
||||
/**
|
||||
* Determines the appropriate endpoint type based on the selected model
|
||||
*
|
||||
*
|
||||
* @param selectedModel - The model identifier string
|
||||
* @param modelInfo - Array of model information
|
||||
* @returns The appropriate endpoint type
|
||||
*/
|
||||
export const determineEndpointType = (
|
||||
selectedModel: string,
|
||||
modelInfo: ModelGroup[]
|
||||
): EndpointType => {
|
||||
export const determineEndpointType = (selectedModel: string, modelInfo: ModelGroup[]): EndpointType => {
|
||||
// Find the model information for the selected model
|
||||
const selectedModelInfo = modelInfo.find(
|
||||
(option) => option.model_group === selectedModel
|
||||
);
|
||||
|
||||
const selectedModelInfo = modelInfo.find((option) => option.model_group === selectedModel);
|
||||
|
||||
// If model info is found and it has a mode, determine the endpoint type
|
||||
if (selectedModelInfo?.mode) {
|
||||
return getEndpointType(selectedModelInfo.mode);
|
||||
}
|
||||
|
||||
|
||||
// Default to chat endpoint if no match is found
|
||||
return EndpointType.CHAT;
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react';
|
||||
import { Typography, Collapse } from 'antd';
|
||||
import React from "react";
|
||||
import { Typography, Collapse } from "antd";
|
||||
|
||||
const { Text } = Typography;
|
||||
const { Panel } = Collapse;
|
||||
|
|
@ -37,24 +37,24 @@ interface MCPEventsDisplayProps {
|
|||
|
||||
const MCPEventsDisplay: React.FC<MCPEventsDisplayProps> = ({ events, className }) => {
|
||||
console.log("MCPEventsDisplay: Received events:", events);
|
||||
|
||||
|
||||
if (!events || events.length === 0) {
|
||||
console.log("MCPEventsDisplay: No events, returning null");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Find the list tools event
|
||||
const toolsEvent = events.find(event =>
|
||||
event.type === 'response.output_item.done' &&
|
||||
event.item?.type === 'mcp_list_tools' &&
|
||||
event.item.tools &&
|
||||
event.item.tools.length > 0
|
||||
const toolsEvent = events.find(
|
||||
(event) =>
|
||||
event.type === "response.output_item.done" &&
|
||||
event.item?.type === "mcp_list_tools" &&
|
||||
event.item.tools &&
|
||||
event.item.tools.length > 0,
|
||||
);
|
||||
|
||||
// Find MCP call events
|
||||
const mcpCallEvents = events.filter(event =>
|
||||
event.type === 'response.output_item.done' &&
|
||||
event.item?.type === 'mcp_call'
|
||||
const mcpCallEvents = events.filter(
|
||||
(event) => event.type === "response.output_item.done" && event.item?.type === "mcp_call",
|
||||
);
|
||||
|
||||
console.log("MCPEventsDisplay: toolsEvent:", toolsEvent);
|
||||
|
|
@ -65,9 +65,8 @@ const MCPEventsDisplay: React.FC<MCPEventsDisplayProps> = ({ events, className }
|
|||
return null;
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div className={`mcp-events-display ${className || ''}`}>
|
||||
<div className={`mcp-events-display ${className || ""}`}>
|
||||
<style jsx>{`
|
||||
.openai-mcp-tools {
|
||||
position: relative;
|
||||
|
|
@ -128,7 +127,8 @@ const MCPEventsDisplay: React.FC<MCPEventsDisplayProps> = ({ events, className }
|
|||
opacity: 0.8;
|
||||
}
|
||||
.tool-item {
|
||||
font-family: ui-monospace, SFMono-Regular, "SF Mono", Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
font-family: ui-monospace, SFMono-Regular, "SF Mono", Monaco, Consolas, "Liberation Mono", "Courier New",
|
||||
monospace;
|
||||
font-size: 13px;
|
||||
color: #4b5563;
|
||||
line-height: 18px;
|
||||
|
|
@ -161,7 +161,8 @@ const MCPEventsDisplay: React.FC<MCPEventsDisplayProps> = ({ events, className }
|
|||
font-size: 12px;
|
||||
}
|
||||
.mcp-json {
|
||||
font-family: ui-monospace, SFMono-Regular, "SF Mono", Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
font-family: ui-monospace, SFMono-Regular, "SF Mono", Monaco, Consolas, "Liberation Mono", "Courier New",
|
||||
monospace;
|
||||
color: #374151;
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
|
|
@ -183,23 +184,21 @@ const MCPEventsDisplay: React.FC<MCPEventsDisplayProps> = ({ events, className }
|
|||
color: #374151;
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
font-family: ui-monospace, SFMono-Regular, "SF Mono", Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
font-family: ui-monospace, SFMono-Regular, "SF Mono", Monaco, Consolas, "Liberation Mono", "Courier New",
|
||||
monospace;
|
||||
}
|
||||
`}</style>
|
||||
<div className="openai-mcp-tools">
|
||||
<div className="openai-vertical-line"></div>
|
||||
<Collapse
|
||||
ghost
|
||||
<Collapse
|
||||
ghost
|
||||
size="small"
|
||||
expandIconPosition="start"
|
||||
defaultActiveKey={toolsEvent ? ['list-tools'] : mcpCallEvents.map((_, index) => `mcp-call-${index}`)}
|
||||
defaultActiveKey={toolsEvent ? ["list-tools"] : mcpCallEvents.map((_, index) => `mcp-call-${index}`)}
|
||||
>
|
||||
{/* List Tools Panel */}
|
||||
{toolsEvent && (
|
||||
<Panel
|
||||
header="List tools"
|
||||
key="list-tools"
|
||||
>
|
||||
<Panel header="List tools" key="list-tools">
|
||||
<div>
|
||||
{toolsEvent.item?.tools?.map((tool, index) => (
|
||||
<div key={index} className="tool-item">
|
||||
|
|
@ -209,13 +208,10 @@ const MCPEventsDisplay: React.FC<MCPEventsDisplayProps> = ({ events, className }
|
|||
</div>
|
||||
</Panel>
|
||||
)}
|
||||
|
||||
|
||||
{/* MCP Call Panels */}
|
||||
{mcpCallEvents.map((callEvent, index) => (
|
||||
<Panel
|
||||
header={callEvent.item?.name || 'Tool call'}
|
||||
key={`mcp-call-${index}`}
|
||||
>
|
||||
<Panel header={callEvent.item?.name || "Tool call"} key={`mcp-call-${index}`}>
|
||||
<div>
|
||||
{/* Request section */}
|
||||
<div className="mcp-section">
|
||||
|
|
@ -234,21 +230,19 @@ const MCPEventsDisplay: React.FC<MCPEventsDisplayProps> = ({ events, className }
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Approved section */}
|
||||
<div className="mcp-section">
|
||||
<div className="mcp-approved">
|
||||
<span className="mcp-checkmark">✓</span> Approved
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Response section */}
|
||||
{callEvent.item?.output && (
|
||||
<div className="mcp-section">
|
||||
<div className="mcp-section-header">Response</div>
|
||||
<div className="mcp-response-content">
|
||||
{callEvent.item.output}
|
||||
</div>
|
||||
<div className="mcp-response-content">{callEvent.item.output}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import React, { useState } from "react";
|
|||
import { Button, Collapse } from "antd";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
||||
import { coy } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
||||
import { coy } from "react-syntax-highlighter/dist/esm/styles/prism";
|
||||
import { DownOutlined, RightOutlined, BulbOutlined } from "@ant-design/icons";
|
||||
|
||||
interface ReasoningContentProps {
|
||||
|
|
@ -16,8 +16,8 @@ const ReasoningContent: React.FC<ReasoningContentProps> = ({ reasoningContent })
|
|||
|
||||
return (
|
||||
<div className="reasoning-content mt-1 mb-2">
|
||||
<Button
|
||||
type="text"
|
||||
<Button
|
||||
type="text"
|
||||
className="flex items-center text-xs text-gray-500 hover:text-gray-700"
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
icon={<BulbOutlined />}
|
||||
|
|
@ -25,16 +25,22 @@ const ReasoningContent: React.FC<ReasoningContentProps> = ({ reasoningContent })
|
|||
{isExpanded ? "Hide reasoning" : "Show reasoning"}
|
||||
{isExpanded ? <DownOutlined className="ml-1" /> : <RightOutlined className="ml-1" />}
|
||||
</Button>
|
||||
|
||||
|
||||
{isExpanded && (
|
||||
<div className="mt-2 p-3 bg-gray-50 border border-gray-200 rounded-md text-sm text-gray-700">
|
||||
<ReactMarkdown
|
||||
components={{
|
||||
code({node, inline, className, children, ...props}: React.ComponentPropsWithoutRef<'code'> & {
|
||||
code({
|
||||
node,
|
||||
inline,
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentPropsWithoutRef<"code"> & {
|
||||
inline?: boolean;
|
||||
node?: any;
|
||||
}) {
|
||||
const match = /language-(\w+)/.exec(className || '');
|
||||
const match = /language-(\w+)/.exec(className || "");
|
||||
return !inline && match ? (
|
||||
<SyntaxHighlighter
|
||||
style={coy as any}
|
||||
|
|
@ -43,14 +49,14 @@ const ReasoningContent: React.FC<ReasoningContentProps> = ({ reasoningContent })
|
|||
className="rounded-md my-2"
|
||||
{...props}
|
||||
>
|
||||
{String(children).replace(/\n$/, '')}
|
||||
{String(children).replace(/\n$/, "")}
|
||||
</SyntaxHighlighter>
|
||||
) : (
|
||||
<code className={`${className} px-1.5 py-0.5 rounded bg-gray-100 text-sm font-mono`} {...props}>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
}
|
||||
},
|
||||
}}
|
||||
>
|
||||
{reasoningContent}
|
||||
|
|
@ -61,4 +67,4 @@ const ReasoningContent: React.FC<ReasoningContentProps> = ({ reasoningContent })
|
|||
);
|
||||
};
|
||||
|
||||
export default ReasoningContent;
|
||||
export default ReasoningContent;
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import React from "react";
|
||||
import { Tooltip } from "antd";
|
||||
import {
|
||||
ClockCircleOutlined,
|
||||
NumberOutlined,
|
||||
ImportOutlined,
|
||||
import {
|
||||
ClockCircleOutlined,
|
||||
NumberOutlined,
|
||||
ImportOutlined,
|
||||
ExportOutlined,
|
||||
ThunderboltOutlined,
|
||||
BulbOutlined,
|
||||
ToolOutlined
|
||||
ToolOutlined,
|
||||
} from "@ant-design/icons";
|
||||
|
||||
export interface TokenUsage {
|
||||
|
|
@ -23,11 +23,7 @@ interface ResponseMetricsProps {
|
|||
toolName?: string;
|
||||
}
|
||||
|
||||
const ResponseMetrics: React.FC<ResponseMetricsProps> = ({
|
||||
timeToFirstToken,
|
||||
usage,
|
||||
toolName
|
||||
}) => {
|
||||
const ResponseMetrics: React.FC<ResponseMetricsProps> = ({ timeToFirstToken, usage, toolName }) => {
|
||||
if (!timeToFirstToken && !usage) return null;
|
||||
|
||||
return (
|
||||
|
|
@ -40,7 +36,7 @@ const ResponseMetrics: React.FC<ResponseMetricsProps> = ({
|
|||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
|
||||
{usage?.promptTokens !== undefined && (
|
||||
<Tooltip title="Prompt tokens">
|
||||
<div className="flex items-center">
|
||||
|
|
@ -49,7 +45,7 @@ const ResponseMetrics: React.FC<ResponseMetricsProps> = ({
|
|||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
|
||||
{usage?.completionTokens !== undefined && (
|
||||
<Tooltip title="Completion tokens">
|
||||
<div className="flex items-center">
|
||||
|
|
@ -58,7 +54,7 @@ const ResponseMetrics: React.FC<ResponseMetricsProps> = ({
|
|||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
|
||||
{usage?.reasoningTokens !== undefined && (
|
||||
<Tooltip title="Reasoning tokens">
|
||||
<div className="flex items-center">
|
||||
|
|
@ -67,7 +63,7 @@ const ResponseMetrics: React.FC<ResponseMetricsProps> = ({
|
|||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
|
||||
{usage?.totalTokens !== undefined && (
|
||||
<Tooltip title="Total tokens">
|
||||
<div className="flex items-center">
|
||||
|
|
@ -89,4 +85,4 @@ const ResponseMetrics: React.FC<ResponseMetricsProps> = ({
|
|||
);
|
||||
};
|
||||
|
||||
export default ResponseMetrics;
|
||||
export default ResponseMetrics;
|
||||
|
|
|
|||
|
|
@ -18,18 +18,18 @@ const ResponsesImageRenderer: React.FC<ResponsesImageRendererProps> = ({ message
|
|||
<div className="mb-2">
|
||||
{isPdf ? (
|
||||
<div className="w-64 h-32 rounded-md border border-gray-200 bg-red-50 flex items-center justify-center">
|
||||
<FilePdfOutlined style={{ fontSize: '48px', color: '#dc2626' }} />
|
||||
<FilePdfOutlined style={{ fontSize: "48px", color: "#dc2626" }} />
|
||||
</div>
|
||||
) : (
|
||||
<img
|
||||
src={message.imagePreviewUrl}
|
||||
alt="User uploaded image"
|
||||
className="max-w-64 rounded-md border border-gray-200 shadow-sm"
|
||||
style={{ maxHeight: '200px' }}
|
||||
<img
|
||||
src={message.imagePreviewUrl}
|
||||
alt="User uploaded image"
|
||||
className="max-w-64 rounded-md border border-gray-200 shadow-sm"
|
||||
style={{ maxHeight: "200px" }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ResponsesImageRenderer;
|
||||
export default ResponsesImageRenderer;
|
||||
|
|
|
|||
|
|
@ -26,14 +26,14 @@ const ResponsesImageUpload: React.FC<ResponsesImageUploadProps> = ({
|
|||
accept="image/*,.pdf"
|
||||
showUploadList={false}
|
||||
className="inline-block"
|
||||
style={{ padding: 0, border: 'none', background: 'none' }}
|
||||
style={{ padding: 0, border: "none", background: "none" }}
|
||||
>
|
||||
<Tooltip title="Attach image or PDF">
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center justify-center w-8 h-8 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-md transition-colors"
|
||||
>
|
||||
<PaperClipOutlined style={{ fontSize: '16px' }} />
|
||||
<PaperClipOutlined style={{ fontSize: "16px" }} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</Dragger>
|
||||
|
|
@ -42,4 +42,4 @@ const ResponsesImageUpload: React.FC<ResponsesImageUploadProps> = ({
|
|||
);
|
||||
};
|
||||
|
||||
export default ResponsesImageUpload;
|
||||
export default ResponsesImageUpload;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ export const convertImageToBase64 = (file: File): Promise<string> => {
|
|||
reader.onload = () => {
|
||||
const result = reader.result as string;
|
||||
// Extract just the base64 data (remove the data:image/...;base64, prefix)
|
||||
const base64Data = result.split(',')[1];
|
||||
const base64Data = result.split(",")[1];
|
||||
resolve(base64Data);
|
||||
};
|
||||
reader.onerror = reject;
|
||||
|
|
@ -16,11 +16,11 @@ export const convertImageToBase64 = (file: File): Promise<string> => {
|
|||
|
||||
export const createMultimodalMessage = async (
|
||||
inputMessage: string,
|
||||
file: File
|
||||
file: File,
|
||||
): Promise<{ role: string; content: MultimodalContent[] }> => {
|
||||
const base64Data = await convertImageToBase64(file);
|
||||
const mimeType = file.type || (file.name.toLowerCase().endsWith('.pdf') ? 'application/pdf' : 'image/jpeg');
|
||||
|
||||
const mimeType = file.type || (file.name.toLowerCase().endsWith(".pdf") ? "application/pdf" : "image/jpeg");
|
||||
|
||||
return {
|
||||
role: "user",
|
||||
content: [
|
||||
|
|
@ -37,30 +37,30 @@ export const createDisplayMessage = (
|
|||
inputMessage: string,
|
||||
hasFile: boolean,
|
||||
filePreviewUrl?: string,
|
||||
fileName?: string
|
||||
fileName?: string,
|
||||
): MessageType => {
|
||||
let attachmentText = "";
|
||||
if (hasFile && fileName) {
|
||||
attachmentText = fileName.toLowerCase().endsWith('.pdf') ? "[PDF attached]" : "[Image attached]";
|
||||
attachmentText = fileName.toLowerCase().endsWith(".pdf") ? "[PDF attached]" : "[Image attached]";
|
||||
}
|
||||
|
||||
const displayMessage: MessageType = {
|
||||
role: "user",
|
||||
content: hasFile ? `${inputMessage} ${attachmentText}` : inputMessage
|
||||
|
||||
const displayMessage: MessageType = {
|
||||
role: "user",
|
||||
content: hasFile ? `${inputMessage} ${attachmentText}` : inputMessage,
|
||||
};
|
||||
|
||||
|
||||
if (hasFile && filePreviewUrl) {
|
||||
displayMessage.imagePreviewUrl = filePreviewUrl;
|
||||
}
|
||||
|
||||
|
||||
return displayMessage;
|
||||
};
|
||||
|
||||
export const shouldShowAttachedImage = (message: MessageType): boolean => {
|
||||
return (
|
||||
message.role === "user" &&
|
||||
typeof message.content === "string" &&
|
||||
(message.content.includes("[Image attached]") || message.content.includes("[PDF attached]")) &&
|
||||
message.role === "user" &&
|
||||
typeof message.content === "string" &&
|
||||
(message.content.includes("[Image attached]") || message.content.includes("[PDF attached]")) &&
|
||||
!!message.imagePreviewUrl
|
||||
);
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -30,24 +30,24 @@ const SessionManagement: React.FC<SessionManagementProps> = ({
|
|||
|
||||
const getSessionDisplay = () => {
|
||||
if (!responsesSessionId) {
|
||||
return useApiSessionManagement ? 'API Session: Ready' : 'UI Session: Ready';
|
||||
return useApiSessionManagement ? "API Session: Ready" : "UI Session: Ready";
|
||||
}
|
||||
|
||||
const sessionPrefix = useApiSessionManagement ? 'Response ID' : 'UI Session';
|
||||
|
||||
const sessionPrefix = useApiSessionManagement ? "Response ID" : "UI Session";
|
||||
const truncatedId = responsesSessionId.slice(0, 10);
|
||||
return `${sessionPrefix}: ${truncatedId}...`;
|
||||
};
|
||||
|
||||
const getSessionDescription = () => {
|
||||
if (!responsesSessionId) {
|
||||
return useApiSessionManagement
|
||||
? 'LiteLLM will manage session using previous_response_id'
|
||||
: 'UI will manage session using chat history';
|
||||
return useApiSessionManagement
|
||||
? "LiteLLM will manage session using previous_response_id"
|
||||
: "UI will manage session using chat history";
|
||||
}
|
||||
|
||||
|
||||
return useApiSessionManagement
|
||||
? 'LiteLLM API session active - context maintained server-side'
|
||||
: 'UI session active - context maintained client-side';
|
||||
? "LiteLLM API session active - context maintained server-side"
|
||||
: "UI session active - context maintained client-side";
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -57,7 +57,7 @@ const SessionManagement: React.FC<SessionManagementProps> = ({
|
|||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium text-gray-700">Session Management</span>
|
||||
<Tooltip title="Choose between LiteLLM API session management (using previous_response_id) or UI-based session management (using chat history)">
|
||||
<InfoCircleOutlined className="text-gray-400" style={{ fontSize: '12px' }} />
|
||||
<InfoCircleOutlined className="text-gray-400" style={{ fontSize: "12px" }} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Switch
|
||||
|
|
@ -70,23 +70,25 @@ const SessionManagement: React.FC<SessionManagementProps> = ({
|
|||
</div>
|
||||
|
||||
{/* Session Status Indicator */}
|
||||
<div className={`text-xs p-2 rounded-md ${
|
||||
responsesSessionId
|
||||
? 'bg-green-50 text-green-700 border border-green-200'
|
||||
: 'bg-blue-50 text-blue-700 border border-blue-200'
|
||||
}`}>
|
||||
<div
|
||||
className={`text-xs p-2 rounded-md ${
|
||||
responsesSessionId
|
||||
? "bg-green-50 text-green-700 border border-green-200"
|
||||
: "bg-blue-50 text-blue-700 border border-blue-200"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-1">
|
||||
<InfoCircleOutlined style={{ fontSize: '12px' }} />
|
||||
<InfoCircleOutlined style={{ fontSize: "12px" }} />
|
||||
{getSessionDisplay()}
|
||||
</div>
|
||||
{responsesSessionId && (
|
||||
<Tooltip
|
||||
<Tooltip
|
||||
title={
|
||||
<div className="text-xs">
|
||||
<div className="mb-1">Copy response ID to continue session:</div>
|
||||
<div className="bg-gray-800 text-gray-100 p-2 rounded font-mono text-xs whitespace-pre-wrap">
|
||||
{`curl -X POST "your-proxy-url/v1/responses" \\
|
||||
{`curl -X POST "your-proxy-url/v1/responses" \\
|
||||
-H "Authorization: Bearer your-api-key" \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '{
|
||||
|
|
@ -98,23 +100,18 @@ const SessionManagement: React.FC<SessionManagementProps> = ({
|
|||
</div>
|
||||
</div>
|
||||
}
|
||||
overlayStyle={{ maxWidth: '500px' }}
|
||||
overlayStyle={{ maxWidth: "500px" }}
|
||||
>
|
||||
<button
|
||||
onClick={handleCopySessionId}
|
||||
className="ml-2 p-1 hover:bg-green-100 rounded transition-colors"
|
||||
>
|
||||
<CopyOutlined style={{ fontSize: '12px' }} />
|
||||
<button onClick={handleCopySessionId} className="ml-2 p-1 hover:bg-green-100 rounded transition-colors">
|
||||
<CopyOutlined style={{ fontSize: "12px" }} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs opacity-75 mt-1">
|
||||
{getSessionDescription()}
|
||||
</div>
|
||||
<div className="text-xs opacity-75 mt-1">{getSessionDescription()}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SessionManagement;
|
||||
export default SessionManagement;
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ export async function makeAnthropicMessagesRequest(
|
|||
traceId?: string,
|
||||
vector_store_ids?: string[],
|
||||
guardrails?: string[],
|
||||
selectedMCPTools?: string[]
|
||||
selectedMCPTools?: string[],
|
||||
) {
|
||||
if (!accessToken) {
|
||||
throw new Error("API key is required");
|
||||
|
|
@ -34,7 +34,7 @@ export async function makeAnthropicMessagesRequest(
|
|||
// Prepare headers with tags and trace ID
|
||||
const headers: Record<string, string> = {};
|
||||
if (tags && tags.length > 0) {
|
||||
headers['x-litellm-tags'] = tags.join(',');
|
||||
headers["x-litellm-tags"] = tags.join(",");
|
||||
}
|
||||
|
||||
const client = new Anthropic({
|
||||
|
|
@ -49,16 +49,21 @@ export async function makeAnthropicMessagesRequest(
|
|||
let firstTokenReceived = false;
|
||||
|
||||
// Format MCP tools if selected
|
||||
const tools = selectedMCPTools && selectedMCPTools.length > 0 ? [{
|
||||
type: "mcp",
|
||||
server_label: "litellm",
|
||||
server_url: `${proxyBaseUrl}/mcp`,
|
||||
require_approval: "never",
|
||||
allowed_tools: selectedMCPTools,
|
||||
headers: {
|
||||
"x-litellm-api-key": `Bearer ${accessToken}`
|
||||
}
|
||||
}] : undefined;
|
||||
const tools =
|
||||
selectedMCPTools && selectedMCPTools.length > 0
|
||||
? [
|
||||
{
|
||||
type: "mcp",
|
||||
server_label: "litellm",
|
||||
server_url: `${proxyBaseUrl}/mcp`,
|
||||
require_approval: "never",
|
||||
allowed_tools: selectedMCPTools,
|
||||
headers: {
|
||||
"x-litellm-api-key": `Bearer ${accessToken}`,
|
||||
},
|
||||
},
|
||||
]
|
||||
: undefined;
|
||||
|
||||
const requestBody: any = {
|
||||
model: selectedModel,
|
||||
|
|
@ -68,7 +73,7 @@ export async function makeAnthropicMessagesRequest(
|
|||
// @ts-ignore - litellm specific parameter
|
||||
litellm_trace_id: traceId,
|
||||
};
|
||||
|
||||
|
||||
if (vector_store_ids) requestBody.vector_store_ids = vector_store_ids;
|
||||
if (guardrails) requestBody.guardrails = guardrails;
|
||||
if (tools) {
|
||||
|
|
@ -82,11 +87,11 @@ export async function makeAnthropicMessagesRequest(
|
|||
|
||||
for await (const messageStreamEvent of stream) {
|
||||
console.log("Stream event:", messageStreamEvent);
|
||||
|
||||
|
||||
// Process content block deltas
|
||||
if (messageStreamEvent.type === 'content_block_delta') {
|
||||
if (messageStreamEvent.type === "content_block_delta") {
|
||||
const delta = messageStreamEvent.delta;
|
||||
|
||||
|
||||
// Measure time to first token
|
||||
if (!firstTokenReceived) {
|
||||
firstTokenReceived = true;
|
||||
|
|
@ -96,20 +101,20 @@ export async function makeAnthropicMessagesRequest(
|
|||
onTimingData(timeToFirstToken);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Handle different types of deltas
|
||||
if (delta.type === 'text_delta') {
|
||||
if (delta.type === "text_delta") {
|
||||
updateTextUI("assistant", delta.text, selectedModel);
|
||||
}
|
||||
// @ts-ignore - reasoning_content might not be in the official types yet
|
||||
else if (delta.type === 'reasoning_delta' && onReasoningContent) {
|
||||
else if (delta.type === "reasoning_delta" && onReasoningContent) {
|
||||
// @ts-ignore
|
||||
onReasoningContent(delta.text);
|
||||
}
|
||||
}
|
||||
|
||||
// Process usage data from message_delta events
|
||||
if (messageStreamEvent.type === 'message_delta' && (messageStreamEvent as any).usage && onUsageData) {
|
||||
if (messageStreamEvent.type === "message_delta" && (messageStreamEvent as any).usage && onUsageData) {
|
||||
const usage = (messageStreamEvent as any).usage;
|
||||
console.log("Usage data found:", usage);
|
||||
const usageData: TokenUsage = {
|
||||
|
|
@ -125,7 +130,7 @@ export async function makeAnthropicMessagesRequest(
|
|||
console.log("Anthropic messages request was cancelled");
|
||||
} else {
|
||||
NotificationManager.fromBackend(
|
||||
`Error occurred while generating model response. Please try again. Error: ${error}`
|
||||
`Error occurred while generating model response. Please try again. Error: ${error}`,
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
|
|
|
|||
|
|
@ -5,144 +5,151 @@ import { TokenUsage } from "../ResponseMetrics";
|
|||
import { getProxyBaseUrl } from "@/components/networking";
|
||||
|
||||
export async function makeOpenAIChatCompletionRequest(
|
||||
chatHistory: { role: string; content: string | any[] }[],
|
||||
updateUI: (chunk: string, model?: string) => void,
|
||||
selectedModel: string,
|
||||
accessToken: string,
|
||||
tags?: string[],
|
||||
signal?: AbortSignal,
|
||||
onReasoningContent?: (content: string) => void,
|
||||
onTimingData?: (timeToFirstToken: number) => void,
|
||||
onUsageData?: (usage: TokenUsage) => void,
|
||||
traceId?: string,
|
||||
vector_store_ids?: string[],
|
||||
guardrails?: string[],
|
||||
selectedMCPTools?: string[],
|
||||
onImageGenerated?: (imageUrl: string, model?: string) => void
|
||||
) {
|
||||
// base url should be the current base_url
|
||||
const isLocal = process.env.NODE_ENV === "development";
|
||||
if (isLocal !== true) {
|
||||
console.log = function () {};
|
||||
}
|
||||
console.log("isLocal:", isLocal);
|
||||
const proxyBaseUrl = getProxyBaseUrl()
|
||||
// Prepare headers with tags and trace ID
|
||||
const headers: Record<string, string> = {};
|
||||
if (tags && tags.length > 0) {
|
||||
headers['x-litellm-tags'] = tags.join(',');
|
||||
}
|
||||
|
||||
const client = new openai.OpenAI({
|
||||
apiKey: accessToken,
|
||||
baseURL: proxyBaseUrl,
|
||||
dangerouslyAllowBrowser: true,
|
||||
defaultHeaders: headers,
|
||||
});
|
||||
|
||||
try {
|
||||
const startTime = Date.now();
|
||||
let firstTokenReceived = false;
|
||||
let timeToFirstToken: number | undefined = undefined;
|
||||
|
||||
// For collecting complete response text
|
||||
let fullResponseContent = "";
|
||||
let fullReasoningContent = "";
|
||||
chatHistory: { role: string; content: string | any[] }[],
|
||||
updateUI: (chunk: string, model?: string) => void,
|
||||
selectedModel: string,
|
||||
accessToken: string,
|
||||
tags?: string[],
|
||||
signal?: AbortSignal,
|
||||
onReasoningContent?: (content: string) => void,
|
||||
onTimingData?: (timeToFirstToken: number) => void,
|
||||
onUsageData?: (usage: TokenUsage) => void,
|
||||
traceId?: string,
|
||||
vector_store_ids?: string[],
|
||||
guardrails?: string[],
|
||||
selectedMCPTools?: string[],
|
||||
onImageGenerated?: (imageUrl: string, model?: string) => void,
|
||||
) {
|
||||
// base url should be the current base_url
|
||||
const isLocal = process.env.NODE_ENV === "development";
|
||||
if (isLocal !== true) {
|
||||
console.log = function () {};
|
||||
}
|
||||
console.log("isLocal:", isLocal);
|
||||
const proxyBaseUrl = getProxyBaseUrl();
|
||||
// Prepare headers with tags and trace ID
|
||||
const headers: Record<string, string> = {};
|
||||
if (tags && tags.length > 0) {
|
||||
headers["x-litellm-tags"] = tags.join(",");
|
||||
}
|
||||
|
||||
// Format MCP tools if selected
|
||||
const tools = selectedMCPTools && selectedMCPTools.length > 0 ? [{
|
||||
type: "mcp",
|
||||
server_label: "litellm",
|
||||
server_url: `${proxyBaseUrl}/mcp`,
|
||||
require_approval: "never",
|
||||
allowed_tools: selectedMCPTools,
|
||||
headers: {
|
||||
"x-litellm-api-key": `Bearer ${accessToken}`
|
||||
}
|
||||
}] : undefined;
|
||||
|
||||
// @ts-ignore
|
||||
const response = await client.chat.completions.create({
|
||||
const client = new openai.OpenAI({
|
||||
apiKey: accessToken,
|
||||
baseURL: proxyBaseUrl,
|
||||
dangerouslyAllowBrowser: true,
|
||||
defaultHeaders: headers,
|
||||
});
|
||||
|
||||
try {
|
||||
const startTime = Date.now();
|
||||
let firstTokenReceived = false;
|
||||
let timeToFirstToken: number | undefined = undefined;
|
||||
|
||||
// For collecting complete response text
|
||||
let fullResponseContent = "";
|
||||
let fullReasoningContent = "";
|
||||
|
||||
// Format MCP tools if selected
|
||||
const tools =
|
||||
selectedMCPTools && selectedMCPTools.length > 0
|
||||
? [
|
||||
{
|
||||
type: "mcp",
|
||||
server_label: "litellm",
|
||||
server_url: `${proxyBaseUrl}/mcp`,
|
||||
require_approval: "never",
|
||||
allowed_tools: selectedMCPTools,
|
||||
headers: {
|
||||
"x-litellm-api-key": `Bearer ${accessToken}`,
|
||||
},
|
||||
},
|
||||
]
|
||||
: undefined;
|
||||
|
||||
// @ts-ignore
|
||||
const response = await client.chat.completions.create(
|
||||
{
|
||||
model: selectedModel,
|
||||
stream: true,
|
||||
stream_options: {
|
||||
include_usage: true,
|
||||
},
|
||||
litellm_trace_id: traceId,
|
||||
litellm_trace_id: traceId,
|
||||
messages: chatHistory as ChatCompletionMessageParam[],
|
||||
...(vector_store_ids ? { vector_store_ids } : {}),
|
||||
...(guardrails ? { guardrails } : {}),
|
||||
...(tools ? { tools, tool_choice: "auto" } : {}),
|
||||
}, { signal });
|
||||
|
||||
for await (const chunk of response) {
|
||||
console.log("Stream chunk:", chunk);
|
||||
|
||||
// Process content and measure time to first token
|
||||
const delta = chunk.choices[0]?.delta as any;
|
||||
|
||||
// Debug what's in the delta
|
||||
console.log("Delta content:", chunk.choices[0]?.delta?.content);
|
||||
console.log("Delta reasoning content:", delta?.reasoning_content);
|
||||
|
||||
// Measure time to first token for either content or reasoning_content
|
||||
if (!firstTokenReceived && (chunk.choices[0]?.delta?.content || (delta && delta.reasoning_content))) {
|
||||
firstTokenReceived = true;
|
||||
timeToFirstToken = Date.now() - startTime;
|
||||
console.log("First token received! Time:", timeToFirstToken, "ms");
|
||||
if (onTimingData) {
|
||||
console.log("Calling onTimingData with:", timeToFirstToken);
|
||||
onTimingData(timeToFirstToken);
|
||||
} else {
|
||||
console.log("onTimingData callback is not defined!");
|
||||
}
|
||||
}
|
||||
|
||||
// Process content
|
||||
if (chunk.choices[0]?.delta?.content) {
|
||||
const content = chunk.choices[0].delta.content;
|
||||
updateUI(content, chunk.model);
|
||||
fullResponseContent += content;
|
||||
}
|
||||
|
||||
// Process image generation if present
|
||||
if (delta && delta.image && onImageGenerated) {
|
||||
console.log("Image generated:", delta.image);
|
||||
onImageGenerated(delta.image.url, chunk.model);
|
||||
}
|
||||
|
||||
// Process reasoning content if present - using type assertion
|
||||
if (delta && delta.reasoning_content) {
|
||||
const reasoningContent = delta.reasoning_content;
|
||||
if (onReasoningContent) {
|
||||
onReasoningContent(reasoningContent);
|
||||
}
|
||||
fullReasoningContent += reasoningContent;
|
||||
}
|
||||
|
||||
// Check for usage data using type assertion
|
||||
const chunkWithUsage = chunk as any;
|
||||
if (chunkWithUsage.usage && onUsageData) {
|
||||
console.log("Usage data found:", chunkWithUsage.usage);
|
||||
const usageData: TokenUsage = {
|
||||
completionTokens: chunkWithUsage.usage.completion_tokens,
|
||||
promptTokens: chunkWithUsage.usage.prompt_tokens,
|
||||
totalTokens: chunkWithUsage.usage.total_tokens,
|
||||
};
|
||||
|
||||
// Check for reasoning tokens
|
||||
if (chunkWithUsage.usage.completion_tokens_details?.reasoning_tokens) {
|
||||
usageData.reasoningTokens = chunkWithUsage.usage.completion_tokens_details.reasoning_tokens;
|
||||
}
|
||||
|
||||
onUsageData(usageData);
|
||||
},
|
||||
{ signal },
|
||||
);
|
||||
|
||||
for await (const chunk of response) {
|
||||
console.log("Stream chunk:", chunk);
|
||||
|
||||
// Process content and measure time to first token
|
||||
const delta = chunk.choices[0]?.delta as any;
|
||||
|
||||
// Debug what's in the delta
|
||||
console.log("Delta content:", chunk.choices[0]?.delta?.content);
|
||||
console.log("Delta reasoning content:", delta?.reasoning_content);
|
||||
|
||||
// Measure time to first token for either content or reasoning_content
|
||||
if (!firstTokenReceived && (chunk.choices[0]?.delta?.content || (delta && delta.reasoning_content))) {
|
||||
firstTokenReceived = true;
|
||||
timeToFirstToken = Date.now() - startTime;
|
||||
console.log("First token received! Time:", timeToFirstToken, "ms");
|
||||
if (onTimingData) {
|
||||
console.log("Calling onTimingData with:", timeToFirstToken);
|
||||
onTimingData(timeToFirstToken);
|
||||
} else {
|
||||
console.log("onTimingData callback is not defined!");
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (signal?.aborted) {
|
||||
console.log("Chat completion request was cancelled");
|
||||
|
||||
// Process content
|
||||
if (chunk.choices[0]?.delta?.content) {
|
||||
const content = chunk.choices[0].delta.content;
|
||||
updateUI(content, chunk.model);
|
||||
fullResponseContent += content;
|
||||
}
|
||||
|
||||
// Process image generation if present
|
||||
if (delta && delta.image && onImageGenerated) {
|
||||
console.log("Image generated:", delta.image);
|
||||
onImageGenerated(delta.image.url, chunk.model);
|
||||
}
|
||||
|
||||
// Process reasoning content if present - using type assertion
|
||||
if (delta && delta.reasoning_content) {
|
||||
const reasoningContent = delta.reasoning_content;
|
||||
if (onReasoningContent) {
|
||||
onReasoningContent(reasoningContent);
|
||||
}
|
||||
fullReasoningContent += reasoningContent;
|
||||
}
|
||||
|
||||
// Check for usage data using type assertion
|
||||
const chunkWithUsage = chunk as any;
|
||||
if (chunkWithUsage.usage && onUsageData) {
|
||||
console.log("Usage data found:", chunkWithUsage.usage);
|
||||
const usageData: TokenUsage = {
|
||||
completionTokens: chunkWithUsage.usage.completion_tokens,
|
||||
promptTokens: chunkWithUsage.usage.prompt_tokens,
|
||||
totalTokens: chunkWithUsage.usage.total_tokens,
|
||||
};
|
||||
|
||||
// Check for reasoning tokens
|
||||
if (chunkWithUsage.usage.completion_tokens_details?.reasoning_tokens) {
|
||||
usageData.reasoningTokens = chunkWithUsage.usage.completion_tokens_details.reasoning_tokens;
|
||||
}
|
||||
|
||||
onUsageData(usageData);
|
||||
}
|
||||
throw error; // Re-throw to allow the caller to handle the error
|
||||
}
|
||||
} catch (error) {
|
||||
if (signal?.aborted) {
|
||||
console.log("Chat completion request was cancelled");
|
||||
}
|
||||
throw error; // Re-throw to allow the caller to handle the error
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,14 +20,12 @@ interface MCPToolsResponse {
|
|||
tools: MCPTool[];
|
||||
}
|
||||
|
||||
export async function fetchAvailableMCPTools(
|
||||
accessToken: string,
|
||||
): Promise<MCPTool[]> {
|
||||
export async function fetchAvailableMCPTools(accessToken: string): Promise<MCPTool[]> {
|
||||
try {
|
||||
const data = await mcpToolsCall(accessToken) as MCPToolsResponse;
|
||||
const data = (await mcpToolsCall(accessToken)) as MCPToolsResponse;
|
||||
return data.tools || [];
|
||||
} catch (error) {
|
||||
console.error("Error fetching MCP tools:", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,9 +10,7 @@ export interface ModelGroup {
|
|||
/**
|
||||
* Fetches available models using modelHubCall and formats them for the selection dropdown.
|
||||
*/
|
||||
export const fetchAvailableModels = async (
|
||||
accessToken: string
|
||||
): Promise<ModelGroup[]> => {
|
||||
export const fetchAvailableModels = async (accessToken: string): Promise<ModelGroup[]> => {
|
||||
try {
|
||||
const fetchedModels = await modelHubCall(accessToken);
|
||||
console.log("model_info:", fetchedModels);
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ export async function makeOpenAIImageEditsRequest(
|
|||
selectedModel: string,
|
||||
accessToken: string,
|
||||
tags?: string[],
|
||||
signal?: AbortSignal
|
||||
signal?: AbortSignal,
|
||||
) {
|
||||
// base url should be the current base_url
|
||||
const isLocal = process.env.NODE_ENV === "development";
|
||||
|
|
@ -18,35 +18,38 @@ export async function makeOpenAIImageEditsRequest(
|
|||
console.log = function () {};
|
||||
}
|
||||
console.log("isLocal:", isLocal);
|
||||
const proxyBaseUrl = getProxyBaseUrl()
|
||||
|
||||
const proxyBaseUrl = getProxyBaseUrl();
|
||||
|
||||
const client = new openai.OpenAI({
|
||||
apiKey: accessToken,
|
||||
baseURL: proxyBaseUrl,
|
||||
dangerouslyAllowBrowser: true,
|
||||
defaultHeaders: tags && tags.length > 0 ? { 'x-litellm-tags': tags.join(',') } : undefined,
|
||||
defaultHeaders: tags && tags.length > 0 ? { "x-litellm-tags": tags.join(",") } : undefined,
|
||||
});
|
||||
|
||||
try {
|
||||
// handle single and multiple images
|
||||
const imagesToProcess = Array.isArray(imageFiles) ? imageFiles : [imageFiles];
|
||||
|
||||
|
||||
// For multiple images, we'll make separate API calls for each image
|
||||
// since OpenAI's edit endpoint processes one image at a time
|
||||
const results = [];
|
||||
|
||||
|
||||
for (let i = 0; i < imagesToProcess.length; i++) {
|
||||
const image = imagesToProcess[i];
|
||||
console.log(`Processing image ${i + 1} of ${imagesToProcess.length}`);
|
||||
|
||||
const response = await client.images.edit({
|
||||
model: selectedModel,
|
||||
image: image,
|
||||
prompt: prompt,
|
||||
}, { signal });
|
||||
|
||||
const response = await client.images.edit(
|
||||
{
|
||||
model: selectedModel,
|
||||
image: image,
|
||||
prompt: prompt,
|
||||
},
|
||||
{ signal },
|
||||
);
|
||||
|
||||
console.log(`Response for image ${i + 1}:`, response.data);
|
||||
|
||||
|
||||
if (response.data && response.data[0]) {
|
||||
// Handle either URL or base64 data from response
|
||||
if (response.data[0].url) {
|
||||
|
|
@ -62,27 +65,26 @@ export async function makeOpenAIImageEditsRequest(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (results.length > 1) {
|
||||
NotificationManager.success(`Successfully processed ${results.length} images`);
|
||||
}
|
||||
|
||||
} catch (error: any) {
|
||||
console.error("Error making image edit request:", error);
|
||||
|
||||
|
||||
if (signal?.aborted) {
|
||||
console.log("Image edits request was cancelled");
|
||||
} else {
|
||||
let errorMessage = "Failed to edit image(s)";
|
||||
|
||||
|
||||
if (error?.error?.message) {
|
||||
errorMessage = error.error.message;
|
||||
} else if (error?.message) {
|
||||
errorMessage = error.message;
|
||||
}
|
||||
|
||||
|
||||
NotificationManager.fromBackend(`Image edit failed: ${errorMessage}`);
|
||||
}
|
||||
throw error; // Re-throw to allow the caller to handle the error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ export async function makeOpenAIImageGenerationRequest(
|
|||
selectedModel: string,
|
||||
accessToken: string,
|
||||
tags?: string[],
|
||||
signal?: AbortSignal
|
||||
signal?: AbortSignal,
|
||||
) {
|
||||
// base url should be the current base_url
|
||||
const isLocal = process.env.NODE_ENV === "development";
|
||||
|
|
@ -17,22 +17,25 @@ export async function makeOpenAIImageGenerationRequest(
|
|||
console.log = function () {};
|
||||
}
|
||||
console.log("isLocal:", isLocal);
|
||||
const proxyBaseUrl = getProxyBaseUrl()
|
||||
const proxyBaseUrl = getProxyBaseUrl();
|
||||
const client = new openai.OpenAI({
|
||||
apiKey: accessToken,
|
||||
baseURL: proxyBaseUrl,
|
||||
dangerouslyAllowBrowser: true,
|
||||
defaultHeaders: tags && tags.length > 0 ? { 'x-litellm-tags': tags.join(',') } : undefined,
|
||||
defaultHeaders: tags && tags.length > 0 ? { "x-litellm-tags": tags.join(",") } : undefined,
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await client.images.generate({
|
||||
model: selectedModel,
|
||||
prompt: prompt,
|
||||
}, { signal });
|
||||
const response = await client.images.generate(
|
||||
{
|
||||
model: selectedModel,
|
||||
prompt: prompt,
|
||||
},
|
||||
{ signal },
|
||||
);
|
||||
|
||||
console.log(response.data);
|
||||
|
||||
|
||||
if (response.data && response.data[0]) {
|
||||
// Handle either URL or base64 data from response
|
||||
if (response.data[0].url) {
|
||||
|
|
|
|||
|
|
@ -56,27 +56,24 @@ export interface StreamProcessCallbacks {
|
|||
onUsage?: (usage: TokenUsage) => void;
|
||||
}
|
||||
|
||||
export const processStreamingResponse = (
|
||||
response: StreamingResponse,
|
||||
callbacks: StreamProcessCallbacks
|
||||
) => {
|
||||
export const processStreamingResponse = (response: StreamingResponse, callbacks: StreamProcessCallbacks) => {
|
||||
// Extract model information if available
|
||||
const model = response.model;
|
||||
|
||||
|
||||
// Process regular content
|
||||
if (response.choices && response.choices.length > 0) {
|
||||
const choice = response.choices[0];
|
||||
|
||||
|
||||
if (choice.delta?.content) {
|
||||
callbacks.onContent(choice.delta.content, model);
|
||||
}
|
||||
|
||||
|
||||
// Process reasoning content if it exists
|
||||
if (choice.delta?.reasoning_content) {
|
||||
callbacks.onReasoningContent(choice.delta.reasoning_content);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Process usage information if it exists and we have a handler
|
||||
if (response.usage && callbacks.onUsage) {
|
||||
console.log("Processing usage data:", response.usage);
|
||||
|
|
@ -85,12 +82,12 @@ export const processStreamingResponse = (
|
|||
promptTokens: response.usage.prompt_tokens,
|
||||
totalTokens: response.usage.total_tokens,
|
||||
};
|
||||
|
||||
|
||||
// Extract reasoning tokens if available
|
||||
if (response.usage.completion_tokens_details?.reasoning_tokens) {
|
||||
usageData.reasoningTokens = response.usage.completion_tokens_details.reasoning_tokens;
|
||||
}
|
||||
|
||||
|
||||
callbacks.onUsage(usageData);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ export async function makeOpenAIResponsesRequest(
|
|||
selectedMCPTools?: string[],
|
||||
previousResponseId?: string | null,
|
||||
onResponseId?: (responseId: string) => void,
|
||||
onMCPEvent?: (event: MCPEvent) => void
|
||||
onMCPEvent?: (event: MCPEvent) => void,
|
||||
) {
|
||||
if (!accessToken) {
|
||||
throw new Error("API key is required");
|
||||
|
|
@ -34,14 +34,14 @@ export async function makeOpenAIResponsesRequest(
|
|||
if (isLocal !== true) {
|
||||
console.log = function () {};
|
||||
}
|
||||
|
||||
const proxyBaseUrl = getProxyBaseUrl()
|
||||
|
||||
const proxyBaseUrl = getProxyBaseUrl();
|
||||
// Prepare headers with tags and trace ID
|
||||
const headers: Record<string, string> = {};
|
||||
if (tags && tags.length > 0) {
|
||||
headers['x-litellm-tags'] = tags.join(',');
|
||||
headers["x-litellm-tags"] = tags.join(",");
|
||||
}
|
||||
|
||||
|
||||
const client = new openai.OpenAI({
|
||||
apiKey: accessToken,
|
||||
baseURL: proxyBaseUrl,
|
||||
|
|
@ -52,46 +52,54 @@ export async function makeOpenAIResponsesRequest(
|
|||
try {
|
||||
const startTime = Date.now();
|
||||
let firstTokenReceived = false;
|
||||
|
||||
|
||||
// Format messages for the API
|
||||
const formattedInput = messages.map(message => {
|
||||
const formattedInput = messages.map((message) => {
|
||||
// If content is already an array (multimodal), use it directly
|
||||
if (Array.isArray(message.content)) {
|
||||
return {
|
||||
role: message.role,
|
||||
content: message.content,
|
||||
type: "message"
|
||||
type: "message",
|
||||
};
|
||||
}
|
||||
// Otherwise, wrap text content in the expected format
|
||||
return {
|
||||
role: message.role,
|
||||
content: message.content,
|
||||
type: "message"
|
||||
type: "message",
|
||||
};
|
||||
});
|
||||
|
||||
// Format MCP tools if selected
|
||||
const tools = selectedMCPTools && selectedMCPTools.length > 0 ? [{
|
||||
type: "mcp",
|
||||
server_label: "litellm",
|
||||
server_url: `litellm_proxy/mcp`,
|
||||
require_approval: "never",
|
||||
allowed_tools: selectedMCPTools,
|
||||
}] : undefined;
|
||||
const tools =
|
||||
selectedMCPTools && selectedMCPTools.length > 0
|
||||
? [
|
||||
{
|
||||
type: "mcp",
|
||||
server_label: "litellm",
|
||||
server_url: `litellm_proxy/mcp`,
|
||||
require_approval: "never",
|
||||
allowed_tools: selectedMCPTools,
|
||||
},
|
||||
]
|
||||
: undefined;
|
||||
|
||||
// Create request to OpenAI responses API
|
||||
// Use 'any' type to avoid TypeScript issues with the experimental API
|
||||
const response = await (client as any).responses.create({
|
||||
model: selectedModel,
|
||||
input: formattedInput,
|
||||
stream: true,
|
||||
litellm_trace_id: traceId,
|
||||
...(previousResponseId ? { previous_response_id: previousResponseId } : {}),
|
||||
...(vector_store_ids ? { vector_store_ids } : {}),
|
||||
...(guardrails ? { guardrails } : {}),
|
||||
...(tools ? { tools, tool_choice: "required" } : {}),
|
||||
}, { signal });
|
||||
const response = await (client as any).responses.create(
|
||||
{
|
||||
model: selectedModel,
|
||||
input: formattedInput,
|
||||
stream: true,
|
||||
litellm_trace_id: traceId,
|
||||
...(previousResponseId ? { previous_response_id: previousResponseId } : {}),
|
||||
...(vector_store_ids ? { vector_store_ids } : {}),
|
||||
...(guardrails ? { guardrails } : {}),
|
||||
...(tools ? { tools, tool_choice: "required" } : {}),
|
||||
},
|
||||
{ signal },
|
||||
);
|
||||
|
||||
let mcpToolUsed = "";
|
||||
|
||||
|
|
@ -99,13 +107,15 @@ export async function makeOpenAIResponsesRequest(
|
|||
console.log("Response event:", event);
|
||||
|
||||
// Use a type-safe approach to handle events
|
||||
if (typeof event === 'object' && event !== null) {
|
||||
if (typeof event === "object" && event !== null) {
|
||||
// Handle MCP events first
|
||||
if (event.type?.startsWith('response.mcp_') ||
|
||||
(event.type === "response.output_item.done" &&
|
||||
(event.item?.type === "mcp_list_tools" || event.item?.type === "mcp_call"))) {
|
||||
if (
|
||||
event.type?.startsWith("response.mcp_") ||
|
||||
(event.type === "response.output_item.done" &&
|
||||
(event.item?.type === "mcp_list_tools" || event.item?.type === "mcp_call"))
|
||||
) {
|
||||
console.log("MCP event received:", event);
|
||||
|
||||
|
||||
if (onMCPEvent) {
|
||||
const mcpEvent: MCPEvent = {
|
||||
type: event.type,
|
||||
|
|
@ -115,18 +125,16 @@ export async function makeOpenAIResponsesRequest(
|
|||
item: event.item,
|
||||
delta: event.delta,
|
||||
arguments: event.arguments,
|
||||
timestamp: Date.now()
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
onMCPEvent(mcpEvent);
|
||||
}
|
||||
|
||||
|
||||
// Continue processing other aspects of the event
|
||||
}
|
||||
|
||||
// Check for MCP tool usage
|
||||
if (event.type === "response.output_item.done" &&
|
||||
event.item?.type === "mcp_call" &&
|
||||
event.item?.name) {
|
||||
if (event.type === "response.output_item.done" && event.item?.type === "mcp_call" && event.item?.name) {
|
||||
mcpToolUsed = event.item.name;
|
||||
console.log("MCP tool used:", mcpToolUsed);
|
||||
}
|
||||
|
|
@ -144,56 +152,56 @@ export async function makeOpenAIResponsesRequest(
|
|||
// skip pure whitespace/newlines
|
||||
if (delta.trim().length > 0) {
|
||||
updateTextUI("assistant", delta, selectedModel);
|
||||
|
||||
|
||||
// Calculate time to first token
|
||||
if (!firstTokenReceived) {
|
||||
firstTokenReceived = true;
|
||||
const timeToFirstToken = Date.now() - startTime;
|
||||
console.log("First token received! Time:", timeToFirstToken, "ms");
|
||||
|
||||
|
||||
if (onTimingData) {
|
||||
onTimingData(timeToFirstToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Handle reasoning content
|
||||
if (event.type === "response.reasoning.delta" && 'delta' in event) {
|
||||
if (event.type === "response.reasoning.delta" && "delta" in event) {
|
||||
const delta = event.delta;
|
||||
if (typeof delta === 'string' && onReasoningContent) {
|
||||
if (typeof delta === "string" && onReasoningContent) {
|
||||
onReasoningContent(delta);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle usage data at the response.completed event
|
||||
if (event.type === "response.completed" && 'response' in event) {
|
||||
if (event.type === "response.completed" && "response" in event) {
|
||||
const response_obj = event.response;
|
||||
const usage = response_obj.usage;
|
||||
console.log("Usage data:", usage);
|
||||
console.log("Response completed event:", response_obj);
|
||||
|
||||
|
||||
// Extract response_id for session management
|
||||
if (response_obj.id && onResponseId) {
|
||||
console.log("Response ID for session management:", response_obj.id);
|
||||
onResponseId(response_obj.id);
|
||||
}
|
||||
|
||||
|
||||
if (usage && onUsageData) {
|
||||
console.log("Usage data:", usage);
|
||||
|
||||
|
||||
// Extract usage data safely
|
||||
const usageData: TokenUsage = {
|
||||
completionTokens: usage.output_tokens,
|
||||
promptTokens: usage.input_tokens,
|
||||
totalTokens: usage.total_tokens
|
||||
totalTokens: usage.total_tokens,
|
||||
};
|
||||
|
||||
|
||||
// Add reasoning tokens if available
|
||||
if (usage.completion_tokens_details?.reasoning_tokens) {
|
||||
usageData.reasoningTokens = usage.completion_tokens_details.reasoning_tokens;
|
||||
}
|
||||
|
||||
|
||||
onUsageData(usageData, mcpToolUsed);
|
||||
}
|
||||
}
|
||||
|
|
@ -205,8 +213,10 @@ export async function makeOpenAIResponsesRequest(
|
|||
if (signal?.aborted) {
|
||||
console.log("Responses API request was cancelled");
|
||||
} else {
|
||||
NotificationManager.fromBackend(`Error occurred while generating model response. Please try again. Error: ${error}`);
|
||||
NotificationManager.fromBackend(
|
||||
`Error occurred while generating model response. Please try again. Error: ${error}`,
|
||||
);
|
||||
}
|
||||
throw error; // Re-throw to allow the caller to handle the error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,42 +2,42 @@
|
|||
|
||||
// Define an enum for the modes as returned in model_info
|
||||
export enum ModelMode {
|
||||
IMAGE_GENERATION = "image_generation",
|
||||
CHAT = "chat",
|
||||
RESPONSES = "responses",
|
||||
IMAGE_EDITS = "image_edits",
|
||||
ANTHROPIC_MESSAGES = "anthropic_messages",
|
||||
// add additional modes as needed
|
||||
}
|
||||
|
||||
// Define an enum for the endpoint types your UI calls
|
||||
export enum EndpointType {
|
||||
IMAGE = "image",
|
||||
CHAT = "chat",
|
||||
RESPONSES = "responses",
|
||||
IMAGE_EDITS = "image_edits",
|
||||
ANTHROPIC_MESSAGES = "anthropic_messages",
|
||||
// add additional endpoint types if required
|
||||
}
|
||||
|
||||
// Create a mapping between the model mode and the corresponding endpoint type
|
||||
export const litellmModeMapping: Record<ModelMode, EndpointType> = {
|
||||
[ModelMode.IMAGE_GENERATION]: EndpointType.IMAGE,
|
||||
[ModelMode.CHAT]: EndpointType.CHAT,
|
||||
[ModelMode.RESPONSES]: EndpointType.RESPONSES,
|
||||
[ModelMode.IMAGE_EDITS]: EndpointType.IMAGE_EDITS,
|
||||
[ModelMode.ANTHROPIC_MESSAGES]: EndpointType.ANTHROPIC_MESSAGES,
|
||||
};
|
||||
IMAGE_GENERATION = "image_generation",
|
||||
CHAT = "chat",
|
||||
RESPONSES = "responses",
|
||||
IMAGE_EDITS = "image_edits",
|
||||
ANTHROPIC_MESSAGES = "anthropic_messages",
|
||||
// add additional modes as needed
|
||||
}
|
||||
|
||||
export const getEndpointType = (mode: string): EndpointType => {
|
||||
// Check if the string mode exists as a key in ModelMode enum
|
||||
console.log("getEndpointType:", mode);
|
||||
if (Object.values(ModelMode).includes(mode as ModelMode)) {
|
||||
const endpointType = litellmModeMapping[mode as ModelMode];
|
||||
console.log("endpointType:", endpointType);
|
||||
return endpointType;
|
||||
}
|
||||
// Define an enum for the endpoint types your UI calls
|
||||
export enum EndpointType {
|
||||
IMAGE = "image",
|
||||
CHAT = "chat",
|
||||
RESPONSES = "responses",
|
||||
IMAGE_EDITS = "image_edits",
|
||||
ANTHROPIC_MESSAGES = "anthropic_messages",
|
||||
// add additional endpoint types if required
|
||||
}
|
||||
|
||||
// else default to chat
|
||||
return EndpointType.CHAT;
|
||||
};
|
||||
// Create a mapping between the model mode and the corresponding endpoint type
|
||||
export const litellmModeMapping: Record<ModelMode, EndpointType> = {
|
||||
[ModelMode.IMAGE_GENERATION]: EndpointType.IMAGE,
|
||||
[ModelMode.CHAT]: EndpointType.CHAT,
|
||||
[ModelMode.RESPONSES]: EndpointType.RESPONSES,
|
||||
[ModelMode.IMAGE_EDITS]: EndpointType.IMAGE_EDITS,
|
||||
[ModelMode.ANTHROPIC_MESSAGES]: EndpointType.ANTHROPIC_MESSAGES,
|
||||
};
|
||||
|
||||
export const getEndpointType = (mode: string): EndpointType => {
|
||||
// Check if the string mode exists as a key in ModelMode enum
|
||||
console.log("getEndpointType:", mode);
|
||||
if (Object.values(ModelMode).includes(mode as ModelMode)) {
|
||||
const endpointType = litellmModeMapping[mode as ModelMode];
|
||||
console.log("endpointType:", endpointType);
|
||||
return endpointType;
|
||||
}
|
||||
|
||||
// else default to chat
|
||||
return EndpointType.CHAT;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -81,4 +81,4 @@ export interface MultimodalContent {
|
|||
type: "input_text" | "input_image";
|
||||
text?: string;
|
||||
image_url?: string;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,5 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import {
|
||||
Card,
|
||||
Title,
|
||||
Text,
|
||||
Button,
|
||||
Callout,
|
||||
TextInput,
|
||||
} from "@tremor/react";
|
||||
import { Card, Title, Text, Button, Callout, TextInput } from "@tremor/react";
|
||||
import { Modal, Form, Input, message, Spin, Select } from "antd";
|
||||
import NotificationsManager from "./molecules/notifications_manager";
|
||||
|
||||
|
|
@ -27,18 +20,14 @@ interface CloudZeroSettingsView {
|
|||
status: string;
|
||||
}
|
||||
|
||||
type ExportType = 'cloudzero' | 'csv';
|
||||
type ExportType = "cloudzero" | "csv";
|
||||
|
||||
const CloudZeroExportModal: React.FC<CloudZeroExportModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
accessToken,
|
||||
}) => {
|
||||
const CloudZeroExportModal: React.FC<CloudZeroExportModalProps> = ({ isOpen, onClose, accessToken }) => {
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [existingSettings, setExistingSettings] = useState<CloudZeroSettingsView | null>(null);
|
||||
const [settingsLoading, setSettingsLoading] = useState(false);
|
||||
const [exportType, setExportType] = useState<ExportType>('cloudzero');
|
||||
const [exportType, setExportType] = useState<ExportType>("cloudzero");
|
||||
const [exportLoading, setExportLoading] = useState(false);
|
||||
|
||||
// Load existing settings when modal opens
|
||||
|
|
@ -69,7 +58,7 @@ const CloudZeroExportModal: React.FC<CloudZeroExportModalProps> = ({
|
|||
} else if (response.status !== 404) {
|
||||
// 404 means no settings configured yet, which is fine
|
||||
const errorData = await response.json();
|
||||
NotificationsManager.fromBackend(`Failed to load existing settings: ${errorData.error || 'Unknown error'}`);
|
||||
NotificationsManager.fromBackend(`Failed to load existing settings: ${errorData.error || "Unknown error"}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading CloudZero settings:", error);
|
||||
|
|
@ -93,7 +82,7 @@ const CloudZeroExportModal: React.FC<CloudZeroExportModalProps> = ({
|
|||
// Add default timezone for backend compatibility
|
||||
const payload = {
|
||||
...values,
|
||||
timezone: "UTC"
|
||||
timezone: "UTC",
|
||||
};
|
||||
|
||||
const response = await fetch(endpoint, {
|
||||
|
|
@ -112,7 +101,7 @@ const CloudZeroExportModal: React.FC<CloudZeroExportModalProps> = ({
|
|||
setExistingSettings({
|
||||
api_key_masked: values.api_key.substring(0, 4) + "****" + values.api_key.slice(-4),
|
||||
connection_id: values.connection_id,
|
||||
status: "configured"
|
||||
status: "configured",
|
||||
});
|
||||
return true;
|
||||
} else {
|
||||
|
|
@ -142,9 +131,9 @@ const CloudZeroExportModal: React.FC<CloudZeroExportModalProps> = ({
|
|||
Authorization: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
body: JSON.stringify({
|
||||
limit: 100000,
|
||||
operation: "replace_hourly"
|
||||
operation: "replace_hourly",
|
||||
}),
|
||||
});
|
||||
|
||||
|
|
@ -179,7 +168,7 @@ const CloudZeroExportModal: React.FC<CloudZeroExportModalProps> = ({
|
|||
};
|
||||
|
||||
const handleExport = async () => {
|
||||
if (exportType === 'cloudzero') {
|
||||
if (exportType === "cloudzero") {
|
||||
// Check if settings exist, if not save them first
|
||||
if (!existingSettings) {
|
||||
const values = await form.validateFields();
|
||||
|
|
@ -194,23 +183,23 @@ const CloudZeroExportModal: React.FC<CloudZeroExportModalProps> = ({
|
|||
|
||||
const handleModalClose = () => {
|
||||
form.resetFields();
|
||||
setExportType('cloudzero');
|
||||
setExportType("cloudzero");
|
||||
setExistingSettings(null);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const exportOptions = [
|
||||
{
|
||||
value: 'cloudzero',
|
||||
value: "cloudzero",
|
||||
label: (
|
||||
<div className="flex items-center gap-2">
|
||||
<img
|
||||
src="/cloudzero.png"
|
||||
alt="CloudZero"
|
||||
<img
|
||||
src="/cloudzero.png"
|
||||
alt="CloudZero"
|
||||
className="w-5 h-5"
|
||||
onError={(e) => {
|
||||
// Fallback to text if image fails to load
|
||||
(e.target as HTMLImageElement).style.display = 'none';
|
||||
(e.target as HTMLImageElement).style.display = "none";
|
||||
}}
|
||||
/>
|
||||
<span>Export to CloudZero</span>
|
||||
|
|
@ -218,11 +207,16 @@ const CloudZeroExportModal: React.FC<CloudZeroExportModalProps> = ({
|
|||
),
|
||||
},
|
||||
{
|
||||
value: 'csv',
|
||||
value: "csv",
|
||||
label: (
|
||||
<div className="flex items-center gap-2">
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||
/>
|
||||
</svg>
|
||||
<span>Export to CSV</span>
|
||||
</div>
|
||||
|
|
@ -231,29 +225,16 @@ const CloudZeroExportModal: React.FC<CloudZeroExportModalProps> = ({
|
|||
];
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="Export Data"
|
||||
open={isOpen}
|
||||
onCancel={handleModalClose}
|
||||
footer={null}
|
||||
width={600}
|
||||
destroyOnClose
|
||||
>
|
||||
<Modal title="Export Data" open={isOpen} onCancel={handleModalClose} footer={null} width={600} destroyOnClose>
|
||||
<div className="space-y-4">
|
||||
{/* Export Type Selection */}
|
||||
<div>
|
||||
<Text className="font-medium mb-2 block">Export Destination</Text>
|
||||
<Select
|
||||
value={exportType}
|
||||
onChange={setExportType}
|
||||
options={exportOptions}
|
||||
className="w-full"
|
||||
size="large"
|
||||
/>
|
||||
<Select value={exportType} onChange={setExportType} options={exportOptions} className="w-full" size="large" />
|
||||
</div>
|
||||
|
||||
{/* CloudZero Configuration */}
|
||||
{exportType === 'cloudzero' && (
|
||||
{exportType === "cloudzero" && (
|
||||
<div>
|
||||
{settingsLoading ? (
|
||||
<div className="flex justify-center py-8">
|
||||
|
|
@ -266,43 +247,39 @@ const CloudZeroExportModal: React.FC<CloudZeroExportModalProps> = ({
|
|||
title="Existing CloudZero Configuration"
|
||||
icon={() => (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
color="green"
|
||||
className="mb-4"
|
||||
>
|
||||
<Text>
|
||||
API Key: {existingSettings.api_key_masked}<br/>
|
||||
API Key: {existingSettings.api_key_masked}
|
||||
<br />
|
||||
Connection ID: {existingSettings.connection_id}
|
||||
</Text>
|
||||
</Callout>
|
||||
)}
|
||||
|
||||
{!existingSettings && (
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item
|
||||
label="CloudZero API Key"
|
||||
name="api_key"
|
||||
rules={[
|
||||
{ required: true, message: "Please enter your CloudZero API key" }
|
||||
]}
|
||||
rules={[{ required: true, message: "Please enter your CloudZero API key" }]}
|
||||
>
|
||||
<TextInput
|
||||
type="password"
|
||||
placeholder="Enter your CloudZero API key"
|
||||
/>
|
||||
<TextInput type="password" placeholder="Enter your CloudZero API key" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="Connection ID"
|
||||
name="connection_id"
|
||||
rules={[
|
||||
{ required: true, message: "Please enter the CloudZero connection ID" }
|
||||
]}
|
||||
rules={[{ required: true, message: "Please enter the CloudZero connection ID" }]}
|
||||
>
|
||||
<TextInput placeholder="Enter CloudZero connection ID" />
|
||||
</Form.Item>
|
||||
|
|
@ -314,7 +291,7 @@ const CloudZeroExportModal: React.FC<CloudZeroExportModalProps> = ({
|
|||
)}
|
||||
|
||||
{/* CSV Export Info */}
|
||||
{exportType === 'csv' && (
|
||||
{exportType === "csv" && (
|
||||
<Callout
|
||||
title="CSV Export"
|
||||
icon={() => (
|
||||
|
|
@ -324,26 +301,17 @@ const CloudZeroExportModal: React.FC<CloudZeroExportModalProps> = ({
|
|||
)}
|
||||
color="blue"
|
||||
>
|
||||
<Text>
|
||||
Export your usage data as a CSV file for analysis in spreadsheet applications.
|
||||
</Text>
|
||||
<Text>Export your usage data as a CSV file for analysis in spreadsheet applications.</Text>
|
||||
</Callout>
|
||||
)}
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex justify-end space-x-2 pt-4">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={handleModalClose}
|
||||
>
|
||||
<Button variant="secondary" onClick={handleModalClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleExport}
|
||||
loading={loading || exportLoading}
|
||||
disabled={loading || exportLoading}
|
||||
>
|
||||
{exportType === 'cloudzero' ? 'Export to CloudZero' : 'Export CSV'}
|
||||
<Button onClick={handleExport} loading={loading || exportLoading} disabled={loading || exportLoading}>
|
||||
{exportType === "cloudzero" ? "Export to CloudZero" : "Export CSV"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -351,4 +319,4 @@ const CloudZeroExportModal: React.FC<CloudZeroExportModalProps> = ({
|
|||
);
|
||||
};
|
||||
|
||||
export default CloudZeroExportModal;
|
||||
export default CloudZeroExportModal;
|
||||
|
|
|
|||
|
|
@ -38,68 +38,66 @@ const AutoRotationView: React.FC<AutoRotationViewProps> = ({
|
|||
|
||||
const content = (
|
||||
<div className="space-y-6">
|
||||
{/* Status Section */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<RefreshIcon className="h-4 w-4 text-blue-600" />
|
||||
<Text className="font-semibold text-gray-900">Auto-Rotation</Text>
|
||||
<Badge color={autoRotate ? "green" : "gray"} size="xs">
|
||||
{autoRotate ? "Enabled" : "Disabled"}
|
||||
</Badge>
|
||||
{autoRotate && rotationInterval && (
|
||||
<>
|
||||
<Text className="text-gray-400">•</Text>
|
||||
<Text className="text-sm text-gray-600">Every {rotationInterval}</Text>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{/* Status Section */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<RefreshIcon className="h-4 w-4 text-blue-600" />
|
||||
<Text className="font-semibold text-gray-900">Auto-Rotation</Text>
|
||||
<Badge color={autoRotate ? "green" : "gray"} size="xs">
|
||||
{autoRotate ? "Enabled" : "Disabled"}
|
||||
</Badge>
|
||||
{autoRotate && rotationInterval && (
|
||||
<>
|
||||
<Text className="text-gray-400">•</Text>
|
||||
<Text className="text-sm text-gray-600">Every {rotationInterval}</Text>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Rotation History - Show if there's any rotation data OR if auto-rotation is enabled */}
|
||||
{(autoRotate || lastRotationAt || keyRotationAt || nextRotationAt) && (
|
||||
<div className="space-y-3">
|
||||
{/* Last Rotation - Show when available */}
|
||||
{lastRotationAt && (
|
||||
<div className="flex items-center gap-2 p-3 bg-gray-50 border border-gray-200 rounded-md">
|
||||
<ClockIcon className="w-4 h-4 text-gray-500" />
|
||||
<div className="flex-1">
|
||||
<Text className="font-medium text-gray-700">Last Rotation</Text>
|
||||
<Text className="text-sm text-gray-600">{formatTimestamp(lastRotationAt)}</Text>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Next Scheduled Rotation - Show when available */}
|
||||
{(keyRotationAt || nextRotationAt) && (
|
||||
<div className="flex items-center gap-2 p-3 bg-gray-50 border border-gray-200 rounded-md">
|
||||
<ClockIcon className="w-4 h-4 text-gray-500" />
|
||||
<div className="flex-1">
|
||||
<Text className="font-medium text-gray-700">Next Scheduled Rotation</Text>
|
||||
<Text className="text-sm text-gray-600">
|
||||
{formatTimestamp(nextRotationAt || keyRotationAt || "")}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* No rotation data message - Only show if auto-rotation is enabled but no data */}
|
||||
{autoRotate && !lastRotationAt && !keyRotationAt && !nextRotationAt && (
|
||||
<div className="flex items-center gap-2 p-3 bg-gray-50 border border-gray-100 rounded-md">
|
||||
<ClockIcon className="w-4 h-4 text-gray-500" />
|
||||
<Text className="text-gray-600">No rotation history available</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Disabled State - Only show if auto-rotation is disabled AND there's no rotation history */}
|
||||
{!autoRotate && !lastRotationAt && !keyRotationAt && !nextRotationAt && (
|
||||
<div className="flex items-center gap-2 p-3 bg-gray-50 border border-gray-100 rounded-md">
|
||||
<RefreshIcon className="w-4 h-4 text-gray-400" />
|
||||
<Text className="text-gray-600">Auto-rotation is not enabled for this key</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Rotation History - Show if there's any rotation data OR if auto-rotation is enabled */}
|
||||
{(autoRotate || lastRotationAt || keyRotationAt || nextRotationAt) && (
|
||||
<div className="space-y-3">
|
||||
{/* Last Rotation - Show when available */}
|
||||
{lastRotationAt && (
|
||||
<div className="flex items-center gap-2 p-3 bg-gray-50 border border-gray-200 rounded-md">
|
||||
<ClockIcon className="w-4 h-4 text-gray-500" />
|
||||
<div className="flex-1">
|
||||
<Text className="font-medium text-gray-700">Last Rotation</Text>
|
||||
<Text className="text-sm text-gray-600">{formatTimestamp(lastRotationAt)}</Text>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Next Scheduled Rotation - Show when available */}
|
||||
{(keyRotationAt || nextRotationAt) && (
|
||||
<div className="flex items-center gap-2 p-3 bg-gray-50 border border-gray-200 rounded-md">
|
||||
<ClockIcon className="w-4 h-4 text-gray-500" />
|
||||
<div className="flex-1">
|
||||
<Text className="font-medium text-gray-700">Next Scheduled Rotation</Text>
|
||||
<Text className="text-sm text-gray-600">{formatTimestamp(nextRotationAt || keyRotationAt || "")}</Text>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* No rotation data message - Only show if auto-rotation is enabled but no data */}
|
||||
{autoRotate && !lastRotationAt && !keyRotationAt && !nextRotationAt && (
|
||||
<div className="flex items-center gap-2 p-3 bg-gray-50 border border-gray-100 rounded-md">
|
||||
<ClockIcon className="w-4 h-4 text-gray-500" />
|
||||
<Text className="text-gray-600">No rotation history available</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Disabled State - Only show if auto-rotation is disabled AND there's no rotation history */}
|
||||
{!autoRotate && !lastRotationAt && !keyRotationAt && !nextRotationAt && (
|
||||
<div className="flex items-center gap-2 p-3 bg-gray-50 border border-gray-100 rounded-md">
|
||||
<RefreshIcon className="w-4 h-4 text-gray-400" />
|
||||
<Text className="text-gray-600">Auto-rotation is not enabled for this key</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
if (variant === "card") {
|
||||
|
|
@ -108,9 +106,7 @@ const AutoRotationView: React.FC<AutoRotationViewProps> = ({
|
|||
<div className="flex items-center gap-2 mb-6">
|
||||
<div>
|
||||
<Text className="font-semibold text-gray-900">Auto-Rotation</Text>
|
||||
<Text className="text-xs text-gray-500">
|
||||
Automatic key rotation settings and status for this key
|
||||
</Text>
|
||||
<Text className="text-xs text-gray-500">Automatic key rotation settings and status for this key</Text>
|
||||
</div>
|
||||
</div>
|
||||
{content}
|
||||
|
|
|
|||
|
|
@ -22,10 +22,10 @@ const KeyLifecycleSettings: React.FC<KeyLifecycleSettingsProps> = ({
|
|||
}) => {
|
||||
// Predefined intervals
|
||||
const predefinedIntervals = ["7d", "30d", "90d", "180d", "365d"];
|
||||
|
||||
|
||||
// Check if current interval is custom
|
||||
const isCustomInterval = rotationInterval && !predefinedIntervals.includes(rotationInterval);
|
||||
|
||||
|
||||
const [showCustomInput, setShowCustomInput] = useState(isCustomInterval);
|
||||
const [customInterval, setCustomInterval] = useState(isCustomInterval ? rotationInterval : "");
|
||||
|
||||
|
|
@ -58,11 +58,7 @@ const KeyLifecycleSettings: React.FC<KeyLifecycleSettingsProps> = ({
|
|||
<InfoCircleOutlined className="text-gray-400 cursor-help text-xs" />
|
||||
</Tooltip>
|
||||
</label>
|
||||
<TextInput
|
||||
name="duration"
|
||||
placeholder="e.g., 30d"
|
||||
className="w-full"
|
||||
/>
|
||||
<TextInput name="duration" placeholder="e.g., 30d" className="w-full" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -110,7 +106,7 @@ const KeyLifecycleSettings: React.FC<KeyLifecycleSettingsProps> = ({
|
|||
<Option value="365d">365 days</Option>
|
||||
<Option value="custom">Custom interval</Option>
|
||||
</Select>
|
||||
|
||||
|
||||
{showCustomInput && (
|
||||
<div className="space-y-1">
|
||||
<TextInput
|
||||
|
|
@ -130,7 +126,8 @@ const KeyLifecycleSettings: React.FC<KeyLifecycleSettingsProps> = ({
|
|||
|
||||
{autoRotationEnabled && (
|
||||
<div className="bg-blue-50 p-3 rounded-md text-sm text-blue-700">
|
||||
When rotation occurs, you'll receive a notification with the new key. The old key will be deactivated after a brief grace period.
|
||||
When rotation occurs, you'll receive a notification with the new key. The old key will be deactivated
|
||||
after a brief grace period.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,17 +1,7 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import { message } from "antd";
|
||||
import { PlusCircleIcon, PencilIcon, TrashIcon } from "@heroicons/react/outline";
|
||||
import {
|
||||
Card,
|
||||
Title,
|
||||
Text,
|
||||
Table,
|
||||
TableHead,
|
||||
TableHeaderCell,
|
||||
TableBody,
|
||||
TableRow,
|
||||
TableCell
|
||||
} from "@tremor/react";
|
||||
import { Card, Title, Text, Table, TableHead, TableHeaderCell, TableBody, TableRow, TableCell } from "@tremor/react";
|
||||
import ModelSelector from "./ModelSelector";
|
||||
import NotificationsManager from "../molecules/notifications_manager";
|
||||
|
||||
|
|
@ -55,7 +45,7 @@ const ModelAliasManager: React.FC<ModelAliasManagerProps> = ({
|
|||
}
|
||||
|
||||
// Check for duplicate alias names
|
||||
if (aliases.some(alias => alias.aliasName === newAlias.aliasName)) {
|
||||
if (aliases.some((alias) => alias.aliasName === newAlias.aliasName)) {
|
||||
NotificationsManager.fromBackend("An alias with this name already exists");
|
||||
return;
|
||||
}
|
||||
|
|
@ -69,17 +59,17 @@ const ModelAliasManager: React.FC<ModelAliasManagerProps> = ({
|
|||
const updatedAliases = [...aliases, newAliasObj];
|
||||
setAliases(updatedAliases);
|
||||
setNewAlias({ aliasName: "", targetModel: "" });
|
||||
|
||||
|
||||
// Convert array back to object format and notify parent
|
||||
const aliasObject: { [key: string]: string } = {};
|
||||
updatedAliases.forEach(alias => {
|
||||
updatedAliases.forEach((alias) => {
|
||||
aliasObject[alias.aliasName] = alias.targetModel;
|
||||
});
|
||||
|
||||
|
||||
if (onAliasUpdate) {
|
||||
onAliasUpdate(aliasObject);
|
||||
}
|
||||
|
||||
|
||||
NotificationsManager.success("Alias added successfully");
|
||||
};
|
||||
|
||||
|
|
@ -96,28 +86,26 @@ const ModelAliasManager: React.FC<ModelAliasManagerProps> = ({
|
|||
}
|
||||
|
||||
// Check for duplicate alias names (excluding current alias)
|
||||
if (aliases.some(alias => alias.id !== editingAlias.id && alias.aliasName === editingAlias.aliasName)) {
|
||||
if (aliases.some((alias) => alias.id !== editingAlias.id && alias.aliasName === editingAlias.aliasName)) {
|
||||
NotificationsManager.fromBackend("An alias with this name already exists");
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedAliases = aliases.map(alias =>
|
||||
alias.id === editingAlias.id ? editingAlias : alias
|
||||
);
|
||||
const updatedAliases = aliases.map((alias) => (alias.id === editingAlias.id ? editingAlias : alias));
|
||||
|
||||
setAliases(updatedAliases);
|
||||
setEditingAlias(null);
|
||||
|
||||
|
||||
// Convert array back to object format and notify parent
|
||||
const aliasObject: { [key: string]: string } = {};
|
||||
updatedAliases.forEach(alias => {
|
||||
updatedAliases.forEach((alias) => {
|
||||
aliasObject[alias.aliasName] = alias.targetModel;
|
||||
});
|
||||
|
||||
|
||||
if (onAliasUpdate) {
|
||||
onAliasUpdate(aliasObject);
|
||||
}
|
||||
|
||||
|
||||
NotificationsManager.success("Alias updated successfully");
|
||||
};
|
||||
|
||||
|
|
@ -126,27 +114,30 @@ const ModelAliasManager: React.FC<ModelAliasManagerProps> = ({
|
|||
};
|
||||
|
||||
const deleteAlias = (aliasId: string) => {
|
||||
const updatedAliases = aliases.filter(alias => alias.id !== aliasId);
|
||||
const updatedAliases = aliases.filter((alias) => alias.id !== aliasId);
|
||||
setAliases(updatedAliases);
|
||||
|
||||
|
||||
// Convert array back to object format and notify parent
|
||||
const aliasObject: { [key: string]: string } = {};
|
||||
updatedAliases.forEach(alias => {
|
||||
updatedAliases.forEach((alias) => {
|
||||
aliasObject[alias.aliasName] = alias.targetModel;
|
||||
});
|
||||
|
||||
|
||||
if (onAliasUpdate) {
|
||||
onAliasUpdate(aliasObject);
|
||||
}
|
||||
|
||||
|
||||
NotificationsManager.success("Alias deleted successfully");
|
||||
};
|
||||
|
||||
// Convert current aliases to object for config example
|
||||
const aliasObject = aliases.reduce((acc, alias) => {
|
||||
acc[alias.aliasName] = alias.targetModel;
|
||||
return acc;
|
||||
}, {} as { [key: string]: string });
|
||||
const aliasObject = aliases.reduce(
|
||||
(acc, alias) => {
|
||||
acc[alias.aliasName] = alias.targetModel;
|
||||
return acc;
|
||||
},
|
||||
{} as { [key: string]: string },
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="mt-4">
|
||||
|
|
@ -169,9 +160,7 @@ const ModelAliasManager: React.FC<ModelAliasManagerProps> = ({
|
|||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-gray-500 mb-1">
|
||||
Target Model
|
||||
</label>
|
||||
<label className="block text-xs text-gray-500 mb-1">Target Model</label>
|
||||
<ModelSelector
|
||||
accessToken={accessToken}
|
||||
value={newAlias.targetModel}
|
||||
|
|
@ -189,7 +178,7 @@ const ModelAliasManager: React.FC<ModelAliasManagerProps> = ({
|
|||
<button
|
||||
onClick={handleAddAlias}
|
||||
disabled={!newAlias.aliasName || !newAlias.targetModel}
|
||||
className={`flex items-center px-4 py-2 rounded-md text-sm ${!newAlias.aliasName || !newAlias.targetModel ? 'bg-gray-300 text-gray-500 cursor-not-allowed' : 'bg-green-600 text-white hover:bg-green-700'}`}
|
||||
className={`flex items-center px-4 py-2 rounded-md text-sm ${!newAlias.aliasName || !newAlias.targetModel ? "bg-gray-300 text-gray-500 cursor-not-allowed" : "bg-green-600 text-white hover:bg-green-700"}`}
|
||||
>
|
||||
<PlusCircleIcon className="w-4 h-4 mr-1" />
|
||||
Add Alias
|
||||
|
|
@ -197,24 +186,16 @@ const ModelAliasManager: React.FC<ModelAliasManagerProps> = ({
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Text className="text-sm font-medium text-gray-700 mb-2">
|
||||
Manage Existing Aliases
|
||||
</Text>
|
||||
|
||||
<Text className="text-sm font-medium text-gray-700 mb-2">Manage Existing Aliases</Text>
|
||||
<div className="rounded-lg custom-border relative mb-6">
|
||||
<div className="overflow-x-auto">
|
||||
<Table className="[&_td]:py-0.5 [&_th]:py-1">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableHeaderCell className="py-1 h-8">
|
||||
Alias Name
|
||||
</TableHeaderCell>
|
||||
<TableHeaderCell className="py-1 h-8">
|
||||
Target Model
|
||||
</TableHeaderCell>
|
||||
<TableHeaderCell className="py-1 h-8">
|
||||
Actions
|
||||
</TableHeaderCell>
|
||||
<TableHeaderCell className="py-1 h-8">Alias Name</TableHeaderCell>
|
||||
<TableHeaderCell className="py-1 h-8">Target Model</TableHeaderCell>
|
||||
<TableHeaderCell className="py-1 h-8">Actions</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
|
|
@ -246,7 +227,7 @@ const ModelAliasManager: React.FC<ModelAliasManagerProps> = ({
|
|||
})
|
||||
}
|
||||
showLabel={false}
|
||||
style={{ height: '32px' }}
|
||||
style={{ height: "32px" }}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="py-0.5 whitespace-nowrap">
|
||||
|
|
@ -268,12 +249,8 @@ const ModelAliasManager: React.FC<ModelAliasManagerProps> = ({
|
|||
</>
|
||||
) : (
|
||||
<>
|
||||
<TableCell className="py-0.5 text-sm text-gray-900">
|
||||
{alias.aliasName}
|
||||
</TableCell>
|
||||
<TableCell className="py-0.5 text-sm text-gray-500">
|
||||
{alias.targetModel}
|
||||
</TableCell>
|
||||
<TableCell className="py-0.5 text-sm text-gray-900">{alias.aliasName}</TableCell>
|
||||
<TableCell className="py-0.5 text-sm text-gray-500">{alias.targetModel}</TableCell>
|
||||
<TableCell className="py-0.5 whitespace-nowrap">
|
||||
<div className="flex space-x-2">
|
||||
<button
|
||||
|
|
@ -296,10 +273,7 @@ const ModelAliasManager: React.FC<ModelAliasManagerProps> = ({
|
|||
))}
|
||||
{aliases.length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={3}
|
||||
className="py-0.5 text-sm text-gray-500 text-center"
|
||||
>
|
||||
<TableCell colSpan={3} className="py-0.5 text-sm text-gray-500 text-center">
|
||||
No aliases added yet. Add a new alias above.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
|
@ -313,9 +287,7 @@ const ModelAliasManager: React.FC<ModelAliasManagerProps> = ({
|
|||
{showExampleConfig && (
|
||||
<Card>
|
||||
<Title className="mb-4">Configuration Example</Title>
|
||||
<Text className="text-gray-600 mb-4">
|
||||
Here's how your current aliases would look in the config:
|
||||
</Text>
|
||||
<Text className="text-gray-600 mb-4">Here's how your current aliases would look in the config:</Text>
|
||||
<div className="bg-gray-100 rounded-lg p-4 font-mono text-sm">
|
||||
<div className="text-gray-700">
|
||||
model_aliases:
|
||||
|
|
@ -340,4 +312,4 @@ const ModelAliasManager: React.FC<ModelAliasManagerProps> = ({
|
|||
);
|
||||
};
|
||||
|
||||
export default ModelAliasManager;
|
||||
export default ModelAliasManager;
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ const ModelSelector: React.FC<ModelSelectorProps> = ({
|
|||
style,
|
||||
className,
|
||||
showLabel = true,
|
||||
labelText = "Select Model"
|
||||
labelText = "Select Model",
|
||||
}) => {
|
||||
const [selectedModel, setSelectedModel] = useState<string | undefined>(value);
|
||||
const [showCustomModelInput, setShowCustomModelInput] = useState<boolean>(false);
|
||||
|
|
@ -38,12 +38,12 @@ const ModelSelector: React.FC<ModelSelectorProps> = ({
|
|||
|
||||
useEffect(() => {
|
||||
if (!accessToken) return;
|
||||
|
||||
|
||||
const loadModels = async () => {
|
||||
try {
|
||||
const uniqueModels = await fetchAvailableModels(accessToken);
|
||||
console.log("Fetched models for selector:", uniqueModels);
|
||||
|
||||
|
||||
if (uniqueModels.length > 0) {
|
||||
setModelInfo(uniqueModels);
|
||||
}
|
||||
|
|
@ -56,7 +56,7 @@ const ModelSelector: React.FC<ModelSelectorProps> = ({
|
|||
}, [accessToken]);
|
||||
|
||||
const onModelChange = (value: string) => {
|
||||
if (value === 'custom') {
|
||||
if (value === "custom") {
|
||||
setShowCustomModelInput(true);
|
||||
setSelectedModel(undefined);
|
||||
} else {
|
||||
|
|
@ -73,7 +73,7 @@ const ModelSelector: React.FC<ModelSelectorProps> = ({
|
|||
if (customModelTimeout.current) {
|
||||
clearTimeout(customModelTimeout.current);
|
||||
}
|
||||
|
||||
|
||||
customModelTimeout.current = setTimeout(() => {
|
||||
setSelectedModel(value);
|
||||
if (onChange) {
|
||||
|
|
@ -94,17 +94,16 @@ const ModelSelector: React.FC<ModelSelectorProps> = ({
|
|||
placeholder={placeholder}
|
||||
onChange={onModelChange}
|
||||
options={[
|
||||
...Array.from(new Set(modelInfo.map(option => option.model_group)))
|
||||
.map((model_group, index) => ({
|
||||
value: model_group,
|
||||
label: model_group,
|
||||
key: index
|
||||
})),
|
||||
{ value: 'custom', label: 'Enter custom model', key: 'custom' }
|
||||
...Array.from(new Set(modelInfo.map((option) => option.model_group))).map((model_group, index) => ({
|
||||
value: model_group,
|
||||
label: model_group,
|
||||
key: index,
|
||||
})),
|
||||
{ value: "custom", label: "Enter custom model", key: "custom" },
|
||||
]}
|
||||
style={{ width: "100%", ...style }}
|
||||
showSearch={true}
|
||||
className={`rounded-md ${className || ''}`}
|
||||
className={`rounded-md ${className || ""}`}
|
||||
disabled={disabled}
|
||||
/>
|
||||
{showCustomModelInput && (
|
||||
|
|
@ -119,4 +118,4 @@ const ModelSelector: React.FC<ModelSelectorProps> = ({
|
|||
);
|
||||
};
|
||||
|
||||
export default ModelSelector;
|
||||
export default ModelSelector;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react';
|
||||
import React from "react";
|
||||
import { Text } from "@tremor/react";
|
||||
import LoggingSettings from "../team/LoggingSettings";
|
||||
|
||||
|
|
@ -15,7 +15,7 @@ export function PremiumLoggingSettings({
|
|||
onChange,
|
||||
premiumUser = false,
|
||||
disabledCallbacks = [],
|
||||
onDisabledCallbacksChange
|
||||
onDisabledCallbacksChange,
|
||||
}: PremiumLoggingSettingsProps) {
|
||||
if (!premiumUser) {
|
||||
return (
|
||||
|
|
@ -30,7 +30,12 @@ export function PremiumLoggingSettings({
|
|||
</div>
|
||||
<div className="p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
|
||||
<Text className="text-sm text-yellow-800">
|
||||
Setting Key/Team logging settings is a LiteLLM Enterprise feature. Global Logging Settings are available for all free users. Get a trial key <a href="https://www.litellm.ai/#pricing" target="_blank" rel="noopener noreferrer" className="underline">here</a>.
|
||||
Setting Key/Team logging settings is a LiteLLM Enterprise feature. Global Logging Settings are available for
|
||||
all free users. Get a trial key{" "}
|
||||
<a href="https://www.litellm.ai/#pricing" target="_blank" rel="noopener noreferrer" className="underline">
|
||||
here
|
||||
</a>
|
||||
.
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -47,4 +52,4 @@ export function PremiumLoggingSettings({
|
|||
);
|
||||
}
|
||||
|
||||
export default PremiumLoggingSettings;
|
||||
export default PremiumLoggingSettings;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react';
|
||||
import React from "react";
|
||||
import { Text } from "@tremor/react";
|
||||
import MCPServerSelector from "../mcp_server_management/MCPServerSelector";
|
||||
|
||||
|
|
@ -15,7 +15,7 @@ export function PremiumMCPSelector({
|
|||
value,
|
||||
accessToken,
|
||||
placeholder = "Select MCP servers",
|
||||
premiumUser = false
|
||||
premiumUser = false,
|
||||
}: PremiumMCPSelectorProps) {
|
||||
if (!premiumUser) {
|
||||
return (
|
||||
|
|
@ -30,21 +30,18 @@ export function PremiumMCPSelector({
|
|||
</div>
|
||||
<div className="p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
|
||||
<Text className="text-sm text-yellow-800">
|
||||
MCP server access control is a LiteLLM Enterprise feature. Get a trial key <a href="https://www.litellm.ai/#pricing" target="_blank" rel="noopener noreferrer" className="underline">here</a>.
|
||||
MCP server access control is a LiteLLM Enterprise feature. Get a trial key{" "}
|
||||
<a href="https://www.litellm.ai/#pricing" target="_blank" rel="noopener noreferrer" className="underline">
|
||||
here
|
||||
</a>
|
||||
.
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<MCPServerSelector
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
accessToken={accessToken}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
);
|
||||
return <MCPServerSelector onChange={onChange} value={value} accessToken={accessToken} placeholder={placeholder} />;
|
||||
}
|
||||
|
||||
export default PremiumMCPSelector;
|
||||
export default PremiumMCPSelector;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react';
|
||||
import React from "react";
|
||||
import { Text } from "@tremor/react";
|
||||
import VectorStoreSelector from "../vector_store_management/VectorStoreSelector";
|
||||
|
||||
|
|
@ -15,7 +15,7 @@ export function PremiumVectorStoreSelector({
|
|||
value,
|
||||
accessToken,
|
||||
placeholder = "Select vector stores",
|
||||
premiumUser = false
|
||||
premiumUser = false,
|
||||
}: PremiumVectorStoreSelectorProps) {
|
||||
if (!premiumUser) {
|
||||
return (
|
||||
|
|
@ -30,21 +30,18 @@ export function PremiumVectorStoreSelector({
|
|||
</div>
|
||||
<div className="p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
|
||||
<Text className="text-sm text-yellow-800">
|
||||
Vector store access control is a LiteLLM Enterprise feature. Get a trial key <a href="https://www.litellm.ai/#pricing" target="_blank" rel="noopener noreferrer" className="underline">here</a>.
|
||||
Vector store access control is a LiteLLM Enterprise feature. Get a trial key{" "}
|
||||
<a href="https://www.litellm.ai/#pricing" target="_blank" rel="noopener noreferrer" className="underline">
|
||||
here
|
||||
</a>
|
||||
.
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<VectorStoreSelector
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
accessToken={accessToken}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
);
|
||||
return <VectorStoreSelector onChange={onChange} value={value} accessToken={accessToken} placeholder={placeholder} />;
|
||||
}
|
||||
|
||||
export default PremiumVectorStoreSelector;
|
||||
export default PremiumVectorStoreSelector;
|
||||
|
|
|
|||
|
|
@ -1,24 +1,24 @@
|
|||
import React from "react"
|
||||
import { Form, Select, Tooltip } from "antd"
|
||||
import { InfoCircleOutlined } from "@ant-design/icons"
|
||||
import React from "react";
|
||||
import { Form, Select, Tooltip } from "antd";
|
||||
import { InfoCircleOutlined } from "@ant-design/icons";
|
||||
|
||||
const { Option } = Select
|
||||
const { Option } = Select;
|
||||
|
||||
interface RateLimitTypeFormItemProps {
|
||||
/** The type of rate limit - either 'tpm' or 'rpm' */
|
||||
type: 'tpm' | 'rpm'
|
||||
type: "tpm" | "rpm";
|
||||
/** The form field name */
|
||||
name: string
|
||||
name: string;
|
||||
/** Whether to show detailed descriptions (default: true) */
|
||||
showDetailedDescriptions?: boolean
|
||||
showDetailedDescriptions?: boolean;
|
||||
/** Additional CSS classes */
|
||||
className?: string
|
||||
className?: string;
|
||||
/** Initial value for the field */
|
||||
initialValue?: string | null
|
||||
initialValue?: string | null;
|
||||
/** Form instance for setting field values */
|
||||
form?: any
|
||||
form?: any;
|
||||
/** Custom onChange handler */
|
||||
onChange?: (value: string) => void
|
||||
onChange?: (value: string) => void;
|
||||
}
|
||||
|
||||
export const RateLimitTypeFormItem: React.FC<RateLimitTypeFormItemProps> = ({
|
||||
|
|
@ -28,29 +28,29 @@ export const RateLimitTypeFormItem: React.FC<RateLimitTypeFormItemProps> = ({
|
|||
className = "",
|
||||
initialValue = null,
|
||||
form,
|
||||
onChange
|
||||
onChange,
|
||||
}) => {
|
||||
const limitTypeUpper = type.toUpperCase()
|
||||
const limitTypeLower = type.toLowerCase()
|
||||
const limitTypeUpper = type.toUpperCase();
|
||||
const limitTypeLower = type.toLowerCase();
|
||||
|
||||
const handleChange = (value: string) => {
|
||||
if (form) {
|
||||
form.setFieldValue(name, value)
|
||||
form.setFieldValue(name, value);
|
||||
}
|
||||
if (onChange) {
|
||||
onChange(value)
|
||||
onChange(value);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const tooltipTitle = `Select 'guaranteed_throughput' to prevent overallocating ${limitTypeUpper} limit when the key belongs to a Team with specific ${limitTypeUpper} limits.`
|
||||
const tooltipTitle = `Select 'guaranteed_throughput' to prevent overallocating ${limitTypeUpper} limit when the key belongs to a Team with specific ${limitTypeUpper} limits.`;
|
||||
|
||||
return (
|
||||
<Form.Item
|
||||
label={
|
||||
<span>
|
||||
{limitTypeUpper} Rate Limit Type{' '}
|
||||
{limitTypeUpper} Rate Limit Type{" "}
|
||||
<Tooltip title={tooltipTitle}>
|
||||
<InfoCircleOutlined style={{ marginLeft: '4px' }} />
|
||||
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
|
|
@ -68,18 +68,20 @@ export const RateLimitTypeFormItem: React.FC<RateLimitTypeFormItemProps> = ({
|
|||
{showDetailedDescriptions ? (
|
||||
<>
|
||||
<Option value="best_effort_throughput" label="Default">
|
||||
<div style={{ padding: '4px 0' }}>
|
||||
<div style={{ padding: "4px 0" }}>
|
||||
<div style={{ fontWeight: 500 }}>Default</div>
|
||||
<div style={{ fontSize: '11px', color: '#6b7280', marginTop: '2px' }}>
|
||||
Best effort throughput - no error if we're overallocating {limitTypeLower} (Team/Key Limits checked at runtime).
|
||||
<div style={{ fontSize: "11px", color: "#6b7280", marginTop: "2px" }}>
|
||||
Best effort throughput - no error if we're overallocating {limitTypeLower} (Team/Key Limits
|
||||
checked at runtime).
|
||||
</div>
|
||||
</div>
|
||||
</Option>
|
||||
<Option value="guaranteed_throughput" label="Guaranteed throughput">
|
||||
<div style={{ padding: '4px 0' }}>
|
||||
<div style={{ padding: "4px 0" }}>
|
||||
<div style={{ fontWeight: 500 }}>Guaranteed throughput</div>
|
||||
<div style={{ fontSize: '11px', color: '#6b7280', marginTop: '2px' }}>
|
||||
Guaranteed throughput - raise an error if we're overallocating {limitTypeLower} (also checks model-specific limits)
|
||||
<div style={{ fontSize: "11px", color: "#6b7280", marginTop: "2px" }}>
|
||||
Guaranteed throughput - raise an error if we're overallocating {limitTypeLower} (also checks
|
||||
model-specific limits)
|
||||
</div>
|
||||
</div>
|
||||
</Option>
|
||||
|
|
@ -92,7 +94,7 @@ export const RateLimitTypeFormItem: React.FC<RateLimitTypeFormItemProps> = ({
|
|||
)}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default RateLimitTypeFormItem
|
||||
export default RateLimitTypeFormItem;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react';
|
||||
import React from "react";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
|
|
@ -23,10 +23,10 @@ interface Column {
|
|||
}
|
||||
|
||||
interface Action<T = any> {
|
||||
icon?: React.ComponentType<any>;
|
||||
onClick: (item: T) => void;
|
||||
condition?: () => boolean;
|
||||
tooltip?: string;
|
||||
icon?: React.ComponentType<any>;
|
||||
onClick: (item: T) => void;
|
||||
condition?: () => boolean;
|
||||
tooltip?: string;
|
||||
}
|
||||
|
||||
interface DeleteModalProps {
|
||||
|
|
@ -35,7 +35,6 @@ interface DeleteModalProps {
|
|||
onCancel: () => void;
|
||||
title: string;
|
||||
message: string;
|
||||
|
||||
}
|
||||
|
||||
interface DataTableProps {
|
||||
|
|
@ -53,11 +52,11 @@ const DataTable: React.FC<DataTableProps> = ({
|
|||
actions,
|
||||
emptyMessage = "No data available",
|
||||
deleteModal,
|
||||
onItemClick
|
||||
onItemClick,
|
||||
}) => {
|
||||
const renderCell = (column: Column, row: any) => {
|
||||
const value = row[column.accessor];
|
||||
|
||||
|
||||
if (column.cellRenderer) {
|
||||
return column.cellRenderer(value, row);
|
||||
}
|
||||
|
|
@ -72,17 +71,8 @@ const DataTable: React.FC<DataTableProps> = ({
|
|||
</Badge>
|
||||
) : (
|
||||
value.map((item: any, index: number) => (
|
||||
<Badge
|
||||
key={index}
|
||||
size="xs"
|
||||
className="mb-1"
|
||||
color="blue"
|
||||
>
|
||||
<Text>
|
||||
{String(item).length > 30
|
||||
? `${String(item).slice(0, 30)}...`
|
||||
: item}
|
||||
</Text>
|
||||
<Badge key={index} size="xs" className="mb-1" color="blue">
|
||||
<Text>{String(item).length > 30 ? `${String(item).slice(0, 30)}...` : item}</Text>
|
||||
</Badge>
|
||||
))
|
||||
)}
|
||||
|
|
@ -90,7 +80,7 @@ const DataTable: React.FC<DataTableProps> = ({
|
|||
);
|
||||
}
|
||||
|
||||
return value?.toString() || '';
|
||||
return value?.toString() || "";
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -101,9 +91,7 @@ const DataTable: React.FC<DataTableProps> = ({
|
|||
{columns.map((column, index) => (
|
||||
<TableHeaderCell key={index}>{column.header}</TableHeaderCell>
|
||||
))}
|
||||
{actions && actions.length > 0 && (
|
||||
<TableHeaderCell>Actions</TableHeaderCell>
|
||||
)}
|
||||
{actions && actions.length > 0 && <TableHeaderCell>Actions</TableHeaderCell>}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
|
||||
|
|
@ -118,13 +106,11 @@ const DataTable: React.FC<DataTableProps> = ({
|
|||
maxWidth: column.width || "4px",
|
||||
whiteSpace: "pre-wrap",
|
||||
overflow: "hidden",
|
||||
...column.style
|
||||
...column.style,
|
||||
}}
|
||||
>
|
||||
{column.accessor === 'id' ? (
|
||||
<Tooltip title={row[column.accessor]}>
|
||||
{renderCell(column, row)}
|
||||
</Tooltip>
|
||||
{column.accessor === "id" ? (
|
||||
<Tooltip title={row[column.accessor]}>{renderCell(column, row)}</Tooltip>
|
||||
) : (
|
||||
renderCell(column, row)
|
||||
)}
|
||||
|
|
@ -132,20 +118,21 @@ const DataTable: React.FC<DataTableProps> = ({
|
|||
))}
|
||||
{actions && actions.length > 0 && (
|
||||
<TableCell>
|
||||
{actions.map((action, actionIndex) => (
|
||||
// @ts-ignore
|
||||
action.condition?.(row) !== false && (
|
||||
<Tooltip key={actionIndex} title={action.tooltip}>
|
||||
<Icon
|
||||
// @ts-ignore
|
||||
icon={action.icon}
|
||||
size="sm"
|
||||
onClick={() => action.onClick(row)}
|
||||
className="cursor-pointer mx-1"
|
||||
/>
|
||||
</Tooltip>
|
||||
)
|
||||
))}
|
||||
{actions.map(
|
||||
(action, actionIndex) =>
|
||||
// @ts-ignore
|
||||
action.condition?.(row) !== false && (
|
||||
<Tooltip key={actionIndex} title={action.tooltip}>
|
||||
<Icon
|
||||
// @ts-ignore
|
||||
icon={action.icon}
|
||||
size="sm"
|
||||
onClick={() => action.onClick(row)}
|
||||
className="cursor-pointer mx-1"
|
||||
/>
|
||||
</Tooltip>
|
||||
),
|
||||
)}
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
|
|
@ -159,10 +146,9 @@ const DataTable: React.FC<DataTableProps> = ({
|
|||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default DataTable;
|
||||
export type { Action, Column, DataTableProps, DeleteModalProps };
|
||||
export type { Action, Column, DataTableProps, DeleteModalProps };
|
||||
|
|
|
|||
|
|
@ -14,11 +14,11 @@ const BudgetDurationDropdown: React.FC<BudgetDurationDropdownProps> = ({
|
|||
value,
|
||||
onChange,
|
||||
className = "",
|
||||
style = {}
|
||||
style = {},
|
||||
}) => {
|
||||
return (
|
||||
<Select
|
||||
style={{ width: '100%', ...style }}
|
||||
style={{ width: "100%", ...style }}
|
||||
value={value || undefined}
|
||||
onChange={onChange}
|
||||
className={className}
|
||||
|
|
@ -33,14 +33,14 @@ const BudgetDurationDropdown: React.FC<BudgetDurationDropdownProps> = ({
|
|||
|
||||
export const getBudgetDurationLabel = (value: string | null | undefined): string => {
|
||||
if (!value) return "Not set";
|
||||
|
||||
|
||||
const budgetDurationMap: Record<string, string> = {
|
||||
"24h": "daily",
|
||||
"7d": "weekly",
|
||||
"30d": "monthly"
|
||||
"30d": "monthly",
|
||||
};
|
||||
|
||||
|
||||
return budgetDurationMap[value] || value;
|
||||
};
|
||||
|
||||
export default BudgetDurationDropdown;
|
||||
export default BudgetDurationDropdown;
|
||||
|
|
|
|||
|
|
@ -16,11 +16,7 @@ const colorNameToHex: { [key: string]: string } = {
|
|||
purple: "#8b5cf6",
|
||||
};
|
||||
|
||||
export const CustomTooltip = ({
|
||||
active,
|
||||
payload,
|
||||
label,
|
||||
}: CustomTooltipProps) => {
|
||||
export const CustomTooltip = ({ active, payload, label }: CustomTooltipProps) => {
|
||||
if (active && payload && payload.length) {
|
||||
const formatCategoryName = (name: string): string => {
|
||||
return name
|
||||
|
|
@ -31,14 +27,9 @@ export const CustomTooltip = ({
|
|||
.join(" ");
|
||||
};
|
||||
|
||||
const getRawValue = (
|
||||
dataPoint: ChartDataPoint,
|
||||
key: string
|
||||
): number | undefined => {
|
||||
const getRawValue = (dataPoint: ChartDataPoint, key: string): number | undefined => {
|
||||
// key is like "metrics.total_tokens"
|
||||
const metricKey = key.substring(
|
||||
key.indexOf(".") + 1
|
||||
) as keyof SpendMetrics;
|
||||
const metricKey = key.substring(key.indexOf(".") + 1) as keyof SpendMetrics;
|
||||
if (dataPoint.metrics && metricKey in dataPoint.metrics) {
|
||||
return dataPoint.metrics[metricKey];
|
||||
}
|
||||
|
|
@ -64,10 +55,7 @@ export const CustomTooltip = ({
|
|||
const colorName = item.color as keyof typeof colorNameToHex;
|
||||
const hexColor = colorNameToHex[colorName] || item.color;
|
||||
return (
|
||||
<div
|
||||
key={dataKey}
|
||||
className="flex items-center justify-between space-x-4"
|
||||
>
|
||||
<div key={dataKey} className="flex items-center justify-between space-x-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span
|
||||
className={`h-2 w-2 shrink-0 rounded-full ring-2 ring-white drop-shadow-md`}
|
||||
|
|
@ -89,13 +77,7 @@ export const CustomTooltip = ({
|
|||
return null;
|
||||
};
|
||||
|
||||
export const CustomLegend = ({
|
||||
categories,
|
||||
colors,
|
||||
}: {
|
||||
categories: string[];
|
||||
colors: string[];
|
||||
}) => {
|
||||
export const CustomLegend = ({ categories, colors }: { categories: string[]; colors: string[] }) => {
|
||||
const formatCategoryName = (name: string): string => {
|
||||
return name
|
||||
.replace("metrics.", "")
|
||||
|
|
@ -112,13 +94,8 @@ export const CustomLegend = ({
|
|||
const hexColor = colorNameToHex[colorName] || colors[idx];
|
||||
return (
|
||||
<div key={category} className="flex items-center space-x-2">
|
||||
<span
|
||||
className={`h-2 w-2 shrink-0 rounded-full ring-4 ring-white`}
|
||||
style={{ backgroundColor: hexColor }}
|
||||
/>
|
||||
<p className="text-sm text-tremor-content dark:text-dark-tremor-content">
|
||||
{formatCategoryName(category)}
|
||||
</p>
|
||||
<span className={`h-2 w-2 shrink-0 rounded-full ring-4 ring-white`} style={{ backgroundColor: hexColor }} />
|
||||
<p className="text-sm text-tremor-content dark:text-dark-tremor-content">{formatCategoryName(category)}</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { Form, Input, InputNumber, Select } from 'antd';
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Form, Input, InputNumber, Select } from "antd";
|
||||
import { TextInput } from "@tremor/react";
|
||||
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||
import { Tooltip } from 'antd';
|
||||
import { getOpenAPISchema } from '../networking';
|
||||
import { InfoCircleOutlined } from "@ant-design/icons";
|
||||
import { Tooltip } from "antd";
|
||||
import { getOpenAPISchema } from "../networking";
|
||||
|
||||
interface SchemaProperty {
|
||||
type?: string;
|
||||
|
|
@ -27,19 +27,18 @@ interface SchemaFormFieldsProps {
|
|||
form: any;
|
||||
overrideLabels?: { [key: string]: string };
|
||||
overrideTooltips?: { [key: string]: string };
|
||||
customValidation?: {
|
||||
[key: string]: (rule: any, value: any) => Promise<void>
|
||||
customValidation?: {
|
||||
[key: string]: (rule: any, value: any) => Promise<void>;
|
||||
};
|
||||
defaultValues?: { [key: string]: any };
|
||||
}
|
||||
|
||||
// Define which fields should be parsed as JSON
|
||||
export const jsonFields = ['metadata', 'config', 'enforced_params', 'aliases'];
|
||||
|
||||
export const jsonFields = ["metadata", "config", "enforced_params", "aliases"];
|
||||
|
||||
// Helper function to determine if a field should be treated as JSON
|
||||
const isJSONField = (key: string, property: SchemaProperty): boolean => {
|
||||
return jsonFields.includes(key) || property.format === 'json';
|
||||
return jsonFields.includes(key) || property.format === "json";
|
||||
};
|
||||
|
||||
// Helper function to validate JSON input
|
||||
|
|
@ -55,29 +54,30 @@ const validateJSON = (value: string): boolean => {
|
|||
|
||||
const getFieldHelp = (key: string, property: SchemaProperty, type: string): string => {
|
||||
// Default help text based on type
|
||||
const defaultHelp = {
|
||||
string: 'Text input',
|
||||
number: 'Numeric input',
|
||||
integer: 'Whole number input',
|
||||
boolean: 'True/False value',
|
||||
}[type] || 'Text input';
|
||||
const defaultHelp =
|
||||
{
|
||||
string: "Text input",
|
||||
number: "Numeric input",
|
||||
integer: "Whole number input",
|
||||
boolean: "True/False value",
|
||||
}[type] || "Text input";
|
||||
|
||||
// Specific field help text
|
||||
const specificHelp: { [key: string]: string } = {
|
||||
max_budget: 'Enter maximum budget in USD (e.g., 100.50)',
|
||||
budget_duration: 'Select a time period for budget reset',
|
||||
tpm_limit: 'Enter maximum tokens per minute (whole number)',
|
||||
rpm_limit: 'Enter maximum requests per minute (whole number)',
|
||||
duration: 'Enter duration (e.g., 30s, 24h, 7d)',
|
||||
max_budget: "Enter maximum budget in USD (e.g., 100.50)",
|
||||
budget_duration: "Select a time period for budget reset",
|
||||
tpm_limit: "Enter maximum tokens per minute (whole number)",
|
||||
rpm_limit: "Enter maximum requests per minute (whole number)",
|
||||
duration: "Enter duration (e.g., 30s, 24h, 7d)",
|
||||
metadata: 'Enter JSON object with key-value pairs\nExample: {"team": "research", "project": "nlp"}',
|
||||
config: 'Enter configuration as JSON object\nExample: {"setting": "value"}',
|
||||
permissions: 'Enter comma-separated permission strings',
|
||||
permissions: "Enter comma-separated permission strings",
|
||||
enforced_params: 'Enter parameters as JSON object\nExample: {"param": "value"}',
|
||||
blocked: 'Enter true/false or specific block conditions',
|
||||
blocked: "Enter true/false or specific block conditions",
|
||||
aliases: 'Enter aliases as JSON object\nExample: {"alias1": "value1", "alias2": "value2"}',
|
||||
models: 'Select one or more model names',
|
||||
key_alias: 'Enter a unique identifier for this key',
|
||||
tags: 'Enter comma-separated tag strings',
|
||||
models: "Select one or more model names",
|
||||
key_alias: "Enter a unique identifier for this key",
|
||||
tags: "Enter comma-separated tag strings",
|
||||
};
|
||||
|
||||
// Get specific help text or use default based on type
|
||||
|
|
@ -87,22 +87,22 @@ const getFieldHelp = (key: string, property: SchemaProperty, type: string): stri
|
|||
if (isJSONField(key, property)) {
|
||||
return `${helpText}\nMust be valid JSON format`;
|
||||
}
|
||||
|
||||
|
||||
if (property.enum) {
|
||||
return `Select from available options\nAllowed values: ${property.enum.join(', ')}`;
|
||||
return `Select from available options\nAllowed values: ${property.enum.join(", ")}`;
|
||||
}
|
||||
|
||||
return helpText;
|
||||
};
|
||||
|
||||
const SchemaFormFields: React.FC<SchemaFormFieldsProps> = ({
|
||||
const SchemaFormFields: React.FC<SchemaFormFieldsProps> = ({
|
||||
schemaComponent,
|
||||
excludedFields = [],
|
||||
form,
|
||||
overrideLabels = {},
|
||||
overrideTooltips = {},
|
||||
customValidation = {},
|
||||
defaultValues = {}
|
||||
defaultValues = {},
|
||||
}) => {
|
||||
const [schemaProperties, setSchemaProperties] = useState<OpenAPISchema | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
|
@ -112,25 +112,24 @@ const SchemaFormFields: React.FC<SchemaFormFieldsProps> = ({
|
|||
try {
|
||||
const schema = await getOpenAPISchema();
|
||||
const componentSchema = schema.components.schemas[schemaComponent];
|
||||
|
||||
|
||||
if (!componentSchema) {
|
||||
throw new Error(`Schema component "${schemaComponent}" not found`);
|
||||
}
|
||||
|
||||
setSchemaProperties(componentSchema);
|
||||
|
||||
|
||||
const defaultFormValues: { [key: string]: any } = {};
|
||||
Object.keys(componentSchema.properties)
|
||||
.filter(key => !excludedFields.includes(key) && defaultValues[key] !== undefined)
|
||||
.forEach(key => {
|
||||
.filter((key) => !excludedFields.includes(key) && defaultValues[key] !== undefined)
|
||||
.forEach((key) => {
|
||||
defaultFormValues[key] = defaultValues[key];
|
||||
});
|
||||
|
||||
|
||||
form.setFieldsValue(defaultFormValues);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Schema fetch error:', error);
|
||||
setError(error instanceof Error ? error.message : 'Failed to fetch schema');
|
||||
console.error("Schema fetch error:", error);
|
||||
setError(error instanceof Error ? error.message : "Failed to fetch schema");
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -142,20 +141,20 @@ const SchemaFormFields: React.FC<SchemaFormFieldsProps> = ({
|
|||
return property.type;
|
||||
}
|
||||
if (property.anyOf) {
|
||||
const types = property.anyOf.map(t => t.type);
|
||||
if (types.includes('number') || types.includes('integer')) return 'number';
|
||||
if (types.includes('string')) return 'string';
|
||||
const types = property.anyOf.map((t) => t.type);
|
||||
if (types.includes("number") || types.includes("integer")) return "number";
|
||||
if (types.includes("string")) return "string";
|
||||
}
|
||||
return 'string';
|
||||
return "string";
|
||||
};
|
||||
|
||||
const renderFormItem = (key: string, property: SchemaProperty) => {
|
||||
const type = getPropertyType(property);
|
||||
const isRequired = schemaProperties?.required?.includes(key);
|
||||
|
||||
|
||||
const label = overrideLabels[key] || property.title || key;
|
||||
const tooltip = overrideTooltips[key] || property.description;
|
||||
|
||||
|
||||
const rules = [];
|
||||
if (isRequired) {
|
||||
rules.push({ required: true, message: `${label} is required` });
|
||||
|
|
@ -167,59 +166,42 @@ const SchemaFormFields: React.FC<SchemaFormFieldsProps> = ({
|
|||
rules.push({
|
||||
validator: async (_: any, value: string) => {
|
||||
if (value && !validateJSON(value)) {
|
||||
throw new Error('Please enter valid JSON');
|
||||
throw new Error("Please enter valid JSON");
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const formLabel = tooltip ? (
|
||||
<span>
|
||||
{label}{' '}
|
||||
{label}{" "}
|
||||
<Tooltip title={tooltip}>
|
||||
<InfoCircleOutlined style={{ marginLeft: '4px' }} />
|
||||
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
|
||||
</Tooltip>
|
||||
</span>
|
||||
) : label;
|
||||
) : (
|
||||
label
|
||||
);
|
||||
|
||||
let inputComponent;
|
||||
if (isJSONField(key, property)) {
|
||||
inputComponent = (
|
||||
<Input.TextArea
|
||||
rows={4}
|
||||
placeholder="Enter as JSON"
|
||||
className="font-mono"
|
||||
/>
|
||||
);
|
||||
inputComponent = <Input.TextArea rows={4} placeholder="Enter as JSON" className="font-mono" />;
|
||||
} else if (property.enum) {
|
||||
inputComponent = (
|
||||
<Select>
|
||||
{property.enum.map(value => (
|
||||
{property.enum.map((value) => (
|
||||
<Select.Option key={value} value={value}>
|
||||
{value}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
} else if (type === 'number' || type === 'integer') {
|
||||
inputComponent = (
|
||||
<InputNumber
|
||||
style={{ width: '100%' }}
|
||||
precision={type === 'integer' ? 0 : undefined}
|
||||
/>
|
||||
);
|
||||
} else if (key === 'duration') {
|
||||
inputComponent = (
|
||||
<TextInput
|
||||
placeholder="eg: 30s, 30h, 30d"
|
||||
/>
|
||||
);
|
||||
} else if (type === "number" || type === "integer") {
|
||||
inputComponent = <InputNumber style={{ width: "100%" }} precision={type === "integer" ? 0 : undefined} />;
|
||||
} else if (key === "duration") {
|
||||
inputComponent = <TextInput placeholder="eg: 30s, 30h, 30d" />;
|
||||
} else {
|
||||
inputComponent = (
|
||||
<TextInput
|
||||
placeholder={tooltip || ''}
|
||||
/>
|
||||
);
|
||||
inputComponent = <TextInput placeholder={tooltip || ""} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -230,11 +212,7 @@ const SchemaFormFields: React.FC<SchemaFormFieldsProps> = ({
|
|||
className="mt-8"
|
||||
rules={rules}
|
||||
initialValue={defaultValues[key]}
|
||||
help={
|
||||
<div className="text-xs text-gray-500">
|
||||
{getFieldHelp(key, property, type)}
|
||||
</div>
|
||||
}
|
||||
help={<div className="text-xs text-gray-500">{getFieldHelp(key, property, type)}</div>}
|
||||
>
|
||||
{inputComponent}
|
||||
</Form.Item>
|
||||
|
|
@ -258,4 +236,4 @@ const SchemaFormFields: React.FC<SchemaFormFieldsProps> = ({
|
|||
);
|
||||
};
|
||||
|
||||
export default SchemaFormFields;
|
||||
export default SchemaFormFields;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Organization } from "../networking";
|
||||
|
||||
export const defaultOrg = {
|
||||
organization_id: null,
|
||||
organization_alias: "Default Organization"
|
||||
} as Organization
|
||||
export const defaultOrg = {
|
||||
organization_id: null,
|
||||
organization_alias: "Default Organization",
|
||||
} as Organization;
|
||||
|
|
|
|||
|
|
@ -1,14 +1,20 @@
|
|||
import { teamListCall, DEFAULT_ORGANIZATION, Organization } from "../networking";
|
||||
|
||||
export const fetchTeams = async (accessToken: string, userID: string | null, userRole: string | null, currentOrg: Organization | null, setTeams: (teams: any[]) => void) => {
|
||||
let givenTeams;
|
||||
if (userRole != "Admin" && userRole != "Admin Viewer") {
|
||||
givenTeams = await teamListCall(accessToken, currentOrg?.organization_id || null, userID)
|
||||
} else {
|
||||
givenTeams = await teamListCall(accessToken, currentOrg?.organization_id || null)
|
||||
}
|
||||
|
||||
console.log(`givenTeams: ${givenTeams}`)
|
||||
export const fetchTeams = async (
|
||||
accessToken: string,
|
||||
userID: string | null,
|
||||
userRole: string | null,
|
||||
currentOrg: Organization | null,
|
||||
setTeams: (teams: any[]) => void,
|
||||
) => {
|
||||
let givenTeams;
|
||||
if (userRole != "Admin" && userRole != "Admin Viewer") {
|
||||
givenTeams = await teamListCall(accessToken, currentOrg?.organization_id || null, userID);
|
||||
} else {
|
||||
givenTeams = await teamListCall(accessToken, currentOrg?.organization_id || null);
|
||||
}
|
||||
|
||||
setTeams(givenTeams)
|
||||
}
|
||||
console.log(`givenTeams: ${givenTeams}`);
|
||||
|
||||
setTeams(givenTeams);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -21,13 +21,13 @@ const TeamDropdown: React.FC<TeamDropdownProps> = ({ teams, value, onChange, dis
|
|||
filterOption={(input, option) => {
|
||||
if (!option) return false;
|
||||
// Get team data from the option key
|
||||
const team = teams?.find(t => t.team_id === option.key);
|
||||
const team = teams?.find((t) => t.team_id === option.key);
|
||||
if (!team) return false;
|
||||
|
||||
|
||||
const searchTerm = input.toLowerCase().trim();
|
||||
const teamAlias = (team.team_alias || '').toLowerCase();
|
||||
const teamId = (team.team_id || '').toLowerCase();
|
||||
|
||||
const teamAlias = (team.team_alias || "").toLowerCase();
|
||||
const teamId = (team.team_id || "").toLowerCase();
|
||||
|
||||
// Search in both team alias and team ID
|
||||
return teamAlias.includes(searchTerm) || teamId.includes(searchTerm);
|
||||
}}
|
||||
|
|
@ -35,12 +35,11 @@ const TeamDropdown: React.FC<TeamDropdownProps> = ({ teams, value, onChange, dis
|
|||
>
|
||||
{teams?.map((team) => (
|
||||
<Select.Option key={team.team_id} value={team.team_id}>
|
||||
<span className="font-medium">{team.team_alias}</span>{" "}
|
||||
<span className="text-gray-500">({team.team_id})</span>
|
||||
<span className="font-medium">{team.team_alias}</span> <span className="text-gray-500">({team.team_id})</span>
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
|
||||
export default TeamDropdown;
|
||||
export default TeamDropdown;
|
||||
|
|
|
|||
|
|
@ -13,17 +13,11 @@ interface UserFormProps {
|
|||
accessToken?: string;
|
||||
}
|
||||
|
||||
const UserForm: React.FC<UserFormProps> = ({
|
||||
form,
|
||||
teams,
|
||||
possibleUIRoles,
|
||||
setPossibleUIRoles,
|
||||
accessToken
|
||||
}) => {
|
||||
const UserForm: React.FC<UserFormProps> = ({ form, teams, possibleUIRoles, setPossibleUIRoles, accessToken }) => {
|
||||
React.useEffect(() => {
|
||||
// Fetch roles if they're not available and we have a setter
|
||||
if (!possibleUIRoles && setPossibleUIRoles && accessToken) {
|
||||
getPossibleUserRoles(accessToken).then(roles => {
|
||||
getPossibleUserRoles(accessToken).then((roles) => {
|
||||
setPossibleUIRoles(roles);
|
||||
});
|
||||
}
|
||||
|
|
@ -31,33 +25,23 @@ const UserForm: React.FC<UserFormProps> = ({
|
|||
|
||||
return (
|
||||
<>
|
||||
<Form.Item
|
||||
label="User Email"
|
||||
name="user_email"
|
||||
rules={[{ required: true, message: "Please input user email" }]}
|
||||
>
|
||||
<Form.Item label="User Email" name="user_email" rules={[{ required: true, message: "Please input user email" }]}>
|
||||
<TextInput placeholder="" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="User Role"
|
||||
name="user_role"
|
||||
rules={[{ required: true, message: "Please select a role" }]}
|
||||
>
|
||||
|
||||
<Form.Item label="User Role" name="user_role" rules={[{ required: true, message: "Please select a role" }]}>
|
||||
<Select>
|
||||
{possibleUIRoles &&
|
||||
Object.entries(possibleUIRoles).map(
|
||||
([role, { ui_label, description }]) => (
|
||||
<AntSelect.Option key={role} value={role} title={ui_label}>
|
||||
<div className="flex">
|
||||
{ui_label}{" "}
|
||||
<p className="ml-2" style={{ color: "gray", fontSize: "12px" }}>
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
</AntSelect.Option>
|
||||
)
|
||||
)}
|
||||
Object.entries(possibleUIRoles).map(([role, { ui_label, description }]) => (
|
||||
<AntSelect.Option key={role} value={role} title={ui_label}>
|
||||
<div className="flex">
|
||||
{ui_label}{" "}
|
||||
<p className="ml-2" style={{ color: "gray", fontSize: "12px" }}>
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
</AntSelect.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
|
|
@ -72,4 +56,4 @@ const UserForm: React.FC<UserFormProps> = ({
|
|||
);
|
||||
};
|
||||
|
||||
export default UserForm;
|
||||
export default UserForm;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { useState, useCallback } from 'react';
|
||||
import { Modal, Form, Button, Select, Tooltip } from 'antd';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { useState, useCallback } from "react";
|
||||
import { Modal, Form, Button, Select, Tooltip } from "antd";
|
||||
import debounce from "lodash/debounce";
|
||||
import { userFilterUICall } from "@/components/networking";
|
||||
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||
import { InfoCircleOutlined } from "@ant-design/icons";
|
||||
interface User {
|
||||
user_id: string;
|
||||
user_email: string;
|
||||
|
|
@ -21,7 +21,6 @@ interface Role {
|
|||
description: string;
|
||||
}
|
||||
|
||||
|
||||
interface FormValues {
|
||||
user_email: string;
|
||||
user_id: string;
|
||||
|
|
@ -38,24 +37,28 @@ interface UserSearchModalProps {
|
|||
defaultRole?: string;
|
||||
}
|
||||
|
||||
const UserSearchModal: React.FC<UserSearchModalProps> = ({
|
||||
isVisible,
|
||||
onCancel,
|
||||
const UserSearchModal: React.FC<UserSearchModalProps> = ({
|
||||
isVisible,
|
||||
onCancel,
|
||||
onSubmit,
|
||||
accessToken,
|
||||
title = "Add Team Member",
|
||||
roles = [
|
||||
{ label: "admin", value: "admin", description: "Admin role. Can create team keys, add members, and manage settings." },
|
||||
{ label: "user", value: "user", description: "User role. Can view team info, but not manage it." }
|
||||
{
|
||||
label: "admin",
|
||||
value: "admin",
|
||||
description: "Admin role. Can create team keys, add members, and manage settings.",
|
||||
},
|
||||
{ label: "user", value: "user", description: "User role. Can view team info, but not manage it." },
|
||||
],
|
||||
defaultRole = "user"
|
||||
defaultRole = "user",
|
||||
}) => {
|
||||
const [form] = Form.useForm<FormValues>();
|
||||
const [userOptions, setUserOptions] = useState<UserOption[]>([]);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [selectedField, setSelectedField] = useState<'user_email' | 'user_id'>('user_email');
|
||||
const [selectedField, setSelectedField] = useState<"user_email" | "user_id">("user_email");
|
||||
|
||||
const fetchUsers = async (searchText: string, fieldName: 'user_email' | 'user_id'): Promise<void> => {
|
||||
const fetchUsers = async (searchText: string, fieldName: "user_email" | "user_id"): Promise<void> => {
|
||||
if (!searchText) {
|
||||
setUserOptions([]);
|
||||
return;
|
||||
|
|
@ -69,29 +72,27 @@ const UserSearchModal: React.FC<UserSearchModalProps> = ({
|
|||
return;
|
||||
}
|
||||
const response = await userFilterUICall(accessToken, params);
|
||||
|
||||
const data: User[] = response
|
||||
const options: UserOption[] = data.map(user => ({
|
||||
label: fieldName === 'user_email'
|
||||
? `${user.user_email}`
|
||||
: `${user.user_id}`,
|
||||
value: fieldName === 'user_email' ? user.user_email : user.user_id,
|
||||
user
|
||||
}));
|
||||
setUserOptions(options);
|
||||
|
||||
const data: User[] = response;
|
||||
const options: UserOption[] = data.map((user) => ({
|
||||
label: fieldName === "user_email" ? `${user.user_email}` : `${user.user_id}`,
|
||||
value: fieldName === "user_email" ? user.user_email : user.user_id,
|
||||
user,
|
||||
}));
|
||||
setUserOptions(options);
|
||||
} catch (error) {
|
||||
console.error('Error fetching users:', error);
|
||||
console.error("Error fetching users:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const debouncedSearch = useCallback(
|
||||
debounce((text: string, fieldName: 'user_email' | 'user_id') => fetchUsers(text, fieldName), 300),
|
||||
[]
|
||||
debounce((text: string, fieldName: "user_email" | "user_id") => fetchUsers(text, fieldName), 300),
|
||||
[],
|
||||
);
|
||||
|
||||
const handleSearch = (value: string, fieldName: 'user_email' | 'user_id'): void => {
|
||||
const handleSearch = (value: string, fieldName: "user_email" | "user_id"): void => {
|
||||
setSelectedField(fieldName);
|
||||
debouncedSearch(value, fieldName);
|
||||
};
|
||||
|
|
@ -101,7 +102,7 @@ const UserSearchModal: React.FC<UserSearchModalProps> = ({
|
|||
form.setFieldsValue({
|
||||
user_email: selectedUser.user_email,
|
||||
user_id: selectedUser.user_id,
|
||||
role: form.getFieldValue('role') // Preserve current role selection
|
||||
role: form.getFieldValue("role"), // Preserve current role selection
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -112,13 +113,7 @@ const UserSearchModal: React.FC<UserSearchModalProps> = ({
|
|||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={title}
|
||||
open={isVisible}
|
||||
onCancel={handleClose}
|
||||
footer={null}
|
||||
width={800}
|
||||
>
|
||||
<Modal title={title} open={isVisible} onCancel={handleClose} footer={null} width={800}>
|
||||
<Form<FormValues>
|
||||
form={form}
|
||||
onFinish={onSubmit}
|
||||
|
|
@ -129,51 +124,39 @@ const UserSearchModal: React.FC<UserSearchModalProps> = ({
|
|||
role: defaultRole,
|
||||
}}
|
||||
>
|
||||
<Form.Item
|
||||
label="Email"
|
||||
name="user_email"
|
||||
className="mb-4"
|
||||
>
|
||||
<Form.Item label="Email" name="user_email" className="mb-4">
|
||||
<Select
|
||||
showSearch
|
||||
className="w-full"
|
||||
className="w-full"
|
||||
placeholder="Search by email"
|
||||
filterOption={false}
|
||||
onSearch={(value) => handleSearch(value, 'user_email')}
|
||||
onSearch={(value) => handleSearch(value, "user_email")}
|
||||
onSelect={(value, option) => handleSelect(value, option as UserOption)}
|
||||
options={selectedField === 'user_email' ? userOptions : []}
|
||||
options={selectedField === "user_email" ? userOptions : []}
|
||||
loading={loading}
|
||||
allowClear
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<div className="text-center mb-4">OR</div>
|
||||
|
||||
<Form.Item
|
||||
label="User ID"
|
||||
name="user_id"
|
||||
className="mb-4"
|
||||
>
|
||||
|
||||
<Form.Item label="User ID" name="user_id" className="mb-4">
|
||||
<Select
|
||||
showSearch
|
||||
className="w-full"
|
||||
placeholder="Search by user ID"
|
||||
placeholder="Search by user ID"
|
||||
filterOption={false}
|
||||
onSearch={(value) => handleSearch(value, 'user_id')}
|
||||
onSearch={(value) => handleSearch(value, "user_id")}
|
||||
onSelect={(value, option) => handleSelect(value, option as UserOption)}
|
||||
options={selectedField === 'user_id' ? userOptions : []}
|
||||
options={selectedField === "user_id" ? userOptions : []}
|
||||
loading={loading}
|
||||
allowClear
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="Member Role"
|
||||
name="role"
|
||||
className="mb-4"
|
||||
>
|
||||
<Form.Item label="Member Role" name="role" className="mb-4">
|
||||
<Select defaultValue={defaultRole}>
|
||||
{roles.map(role => (
|
||||
{roles.map((role) => (
|
||||
<Select.Option key={role.value} value={role.value}>
|
||||
<Tooltip title={role.description}>
|
||||
<span className="font-medium">{role.label}</span>
|
||||
|
|
@ -194,4 +177,4 @@ const UserSearchModal: React.FC<UserSearchModalProps> = ({
|
|||
);
|
||||
};
|
||||
|
||||
export default UserSearchModal;
|
||||
export default UserSearchModal;
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
// useBaseUrl.ts
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
export const useBaseUrl = () => {
|
||||
const [baseUrl, setBaseUrl] = useState("http://localhost:4000");
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
if (typeof window !== "undefined") {
|
||||
const { protocol, host } = window.location;
|
||||
setBaseUrl(`${protocol}//${host}`);
|
||||
}
|
||||
|
|
@ -14,4 +14,4 @@ export const useBaseUrl = () => {
|
|||
return baseUrl;
|
||||
};
|
||||
|
||||
export const defaultPageSize = 25;
|
||||
export const defaultPageSize = 25;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useState, useEffect } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { Button, Modal, Form, Input, message, Select, InputNumber, Select as Select2 } from "antd"
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Button, Modal, Form, Input, message, Select, InputNumber, Select as Select2 } from "antd";
|
||||
import {
|
||||
Button as Button2,
|
||||
Text,
|
||||
|
|
@ -10,52 +10,52 @@ import {
|
|||
AccordionHeader,
|
||||
AccordionBody,
|
||||
Title,
|
||||
} from "@tremor/react"
|
||||
import OnboardingModal from "./onboarding_link"
|
||||
import { InvitationLink } from "./onboarding_link"
|
||||
} from "@tremor/react";
|
||||
import OnboardingModal from "./onboarding_link";
|
||||
import { InvitationLink } from "./onboarding_link";
|
||||
import {
|
||||
userCreateCall,
|
||||
modelAvailableCall,
|
||||
invitationCreateCall,
|
||||
getProxyUISettings,
|
||||
getProxyBaseUrl,
|
||||
} from "./networking"
|
||||
import BulkCreateUsers from "./bulk_create_users_button"
|
||||
const { Option } = Select
|
||||
import { Tooltip } from "antd"
|
||||
import { InfoCircleOutlined } from "@ant-design/icons"
|
||||
import { getModelDisplayName } from "./key_team_helpers/fetch_available_models_team_key"
|
||||
import { useQueryClient } from "@tanstack/react-query"
|
||||
import NotificationsManager from "./molecules/notifications_manager"
|
||||
} from "./networking";
|
||||
import BulkCreateUsers from "./bulk_create_users_button";
|
||||
const { Option } = Select;
|
||||
import { Tooltip } from "antd";
|
||||
import { InfoCircleOutlined } from "@ant-design/icons";
|
||||
import { getModelDisplayName } from "./key_team_helpers/fetch_available_models_team_key";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import NotificationsManager from "./molecules/notifications_manager";
|
||||
|
||||
// Helper function to generate UUID compatible across all environments
|
||||
const generateUUID = (): string => {
|
||||
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
||||
return crypto.randomUUID()
|
||||
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
||||
return crypto.randomUUID();
|
||||
}
|
||||
// Fallback UUID generation for environments without crypto.randomUUID
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
||||
const r = Math.random() * 16 | 0
|
||||
const v = c == 'x' ? r : (r & 0x3 | 0x8)
|
||||
return v.toString(16)
|
||||
})
|
||||
}
|
||||
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
|
||||
const r = (Math.random() * 16) | 0;
|
||||
const v = c == "x" ? r : (r & 0x3) | 0x8;
|
||||
return v.toString(16);
|
||||
});
|
||||
};
|
||||
|
||||
interface CreateuserProps {
|
||||
userID: string
|
||||
accessToken: string
|
||||
teams: any[] | null
|
||||
possibleUIRoles: null | Record<string, Record<string, string>>
|
||||
onUserCreated?: (userId: string) => void
|
||||
isEmbedded?: boolean
|
||||
userID: string;
|
||||
accessToken: string;
|
||||
teams: any[] | null;
|
||||
possibleUIRoles: null | Record<string, Record<string, string>>;
|
||||
onUserCreated?: (userId: string) => void;
|
||||
isEmbedded?: boolean;
|
||||
}
|
||||
|
||||
// Define an interface for the UI settings
|
||||
interface UISettings {
|
||||
PROXY_BASE_URL: string | null
|
||||
PROXY_LOGOUT_URL: string | null
|
||||
DEFAULT_TEAM_DISABLED: boolean
|
||||
SSO_ENABLED: boolean
|
||||
PROXY_BASE_URL: string | null;
|
||||
PROXY_LOGOUT_URL: string | null;
|
||||
DEFAULT_TEAM_DISABLED: boolean;
|
||||
SSO_ENABLED: boolean;
|
||||
}
|
||||
|
||||
const Createuser: React.FC<CreateuserProps> = ({
|
||||
|
|
@ -66,91 +66,91 @@ const Createuser: React.FC<CreateuserProps> = ({
|
|||
onUserCreated,
|
||||
isEmbedded = false,
|
||||
}) => {
|
||||
const queryClient = useQueryClient()
|
||||
const [uiSettings, setUISettings] = useState<UISettings | null>(null)
|
||||
const [form] = Form.useForm()
|
||||
const [isModalVisible, setIsModalVisible] = useState(false)
|
||||
const [apiuser, setApiuser] = useState<boolean>(false)
|
||||
const [userModels, setUserModels] = useState<string[]>([])
|
||||
const [isInvitationLinkModalVisible, setIsInvitationLinkModalVisible] = useState(false)
|
||||
const [invitationLinkData, setInvitationLinkData] = useState<InvitationLink | null>(null)
|
||||
const [baseUrl, setBaseUrl] = useState<string | null>(null)
|
||||
const queryClient = useQueryClient();
|
||||
const [uiSettings, setUISettings] = useState<UISettings | null>(null);
|
||||
const [form] = Form.useForm();
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
const [apiuser, setApiuser] = useState<boolean>(false);
|
||||
const [userModels, setUserModels] = useState<string[]>([]);
|
||||
const [isInvitationLinkModalVisible, setIsInvitationLinkModalVisible] = useState(false);
|
||||
const [invitationLinkData, setInvitationLinkData] = useState<InvitationLink | null>(null);
|
||||
const [baseUrl, setBaseUrl] = useState<string | null>(null);
|
||||
// get all models
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const userRole = "any" // You may need to get the user role dynamically
|
||||
const modelDataResponse = await modelAvailableCall(accessToken, userID, userRole)
|
||||
const userRole = "any"; // You may need to get the user role dynamically
|
||||
const modelDataResponse = await modelAvailableCall(accessToken, userID, userRole);
|
||||
// Assuming modelDataResponse.data contains an array of model objects with a 'model_name' property
|
||||
const availableModels = []
|
||||
const availableModels = [];
|
||||
for (let i = 0; i < modelDataResponse.data.length; i++) {
|
||||
const model = modelDataResponse.data[i]
|
||||
availableModels.push(model.id)
|
||||
const model = modelDataResponse.data[i];
|
||||
availableModels.push(model.id);
|
||||
}
|
||||
console.log("Model data response:", modelDataResponse.data)
|
||||
console.log("Available models:", availableModels)
|
||||
console.log("Model data response:", modelDataResponse.data);
|
||||
console.log("Available models:", availableModels);
|
||||
|
||||
// Assuming modelDataResponse.data contains an array of model names
|
||||
setUserModels(availableModels)
|
||||
setUserModels(availableModels);
|
||||
|
||||
// get ui settings
|
||||
const uiSettingsResponse = await getProxyUISettings(accessToken)
|
||||
console.log("uiSettingsResponse:", uiSettingsResponse)
|
||||
const uiSettingsResponse = await getProxyUISettings(accessToken);
|
||||
console.log("uiSettingsResponse:", uiSettingsResponse);
|
||||
|
||||
setUISettings(uiSettingsResponse)
|
||||
setUISettings(uiSettingsResponse);
|
||||
} catch (error) {
|
||||
console.error("Error fetching model data:", error)
|
||||
console.error("Error fetching model data:", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
setBaseUrl(getProxyBaseUrl())
|
||||
setBaseUrl(getProxyBaseUrl());
|
||||
|
||||
fetchData() // Call the function to fetch model data when the component mounts
|
||||
}, []) // Empty dependency array to run only once
|
||||
fetchData(); // Call the function to fetch model data when the component mounts
|
||||
}, []); // Empty dependency array to run only once
|
||||
|
||||
const handleOk = () => {
|
||||
setIsModalVisible(false)
|
||||
form.resetFields()
|
||||
}
|
||||
setIsModalVisible(false);
|
||||
form.resetFields();
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setIsModalVisible(false)
|
||||
setApiuser(false)
|
||||
form.resetFields()
|
||||
}
|
||||
setIsModalVisible(false);
|
||||
setApiuser(false);
|
||||
form.resetFields();
|
||||
};
|
||||
|
||||
const handleCreate = async (formValues: { user_id: string; models?: string[]; user_role: string }) => {
|
||||
try {
|
||||
NotificationsManager.info("Making API Call")
|
||||
NotificationsManager.info("Making API Call");
|
||||
if (!isEmbedded) {
|
||||
setIsModalVisible(true)
|
||||
setIsModalVisible(true);
|
||||
}
|
||||
if ((!formValues.models || formValues.models.length === 0) && formValues.user_role !== "proxy_admin") {
|
||||
console.log("formValues.user_role", formValues.user_role)
|
||||
console.log("formValues.user_role", formValues.user_role);
|
||||
// If models is empty or undefined, set it to "no-default-models"
|
||||
formValues.models = ["no-default-models"]
|
||||
formValues.models = ["no-default-models"];
|
||||
}
|
||||
console.log("formValues in create user:", formValues)
|
||||
const response = await userCreateCall(accessToken, null, formValues)
|
||||
await queryClient.invalidateQueries({ queryKey: ["userList"] })
|
||||
console.log("user create Response:", response)
|
||||
setApiuser(true)
|
||||
const user_id = response.data?.user_id || response.user_id
|
||||
console.log("formValues in create user:", formValues);
|
||||
const response = await userCreateCall(accessToken, null, formValues);
|
||||
await queryClient.invalidateQueries({ queryKey: ["userList"] });
|
||||
console.log("user create Response:", response);
|
||||
setApiuser(true);
|
||||
const user_id = response.data?.user_id || response.user_id;
|
||||
|
||||
// Call the callback if provided (for embedded mode)
|
||||
if (onUserCreated && isEmbedded) {
|
||||
onUserCreated(user_id)
|
||||
form.resetFields()
|
||||
return // Skip the invitation flow when embedded
|
||||
onUserCreated(user_id);
|
||||
form.resetFields();
|
||||
return; // Skip the invitation flow when embedded
|
||||
}
|
||||
|
||||
// only do invite link flow if sso is not enabled
|
||||
if (!uiSettings?.SSO_ENABLED) {
|
||||
invitationCreateCall(accessToken, user_id).then((data) => {
|
||||
data.has_user_setup_sso = false
|
||||
setInvitationLinkData(data)
|
||||
setIsInvitationLinkModalVisible(true)
|
||||
})
|
||||
data.has_user_setup_sso = false;
|
||||
setInvitationLinkData(data);
|
||||
setIsInvitationLinkModalVisible(true);
|
||||
});
|
||||
} else {
|
||||
// create an InvitationLink Object for this user for the SSO flow
|
||||
// for SSO the invite link is the proxy base url since the User just needs to login
|
||||
|
|
@ -165,20 +165,20 @@ const Createuser: React.FC<CreateuserProps> = ({
|
|||
updated_at: new Date(),
|
||||
updated_by: userID,
|
||||
has_user_setup_sso: true,
|
||||
}
|
||||
setInvitationLinkData(invitationLink)
|
||||
setIsInvitationLinkModalVisible(true)
|
||||
};
|
||||
setInvitationLinkData(invitationLink);
|
||||
setIsInvitationLinkModalVisible(true);
|
||||
}
|
||||
|
||||
NotificationsManager.success("API user Created")
|
||||
form.resetFields()
|
||||
localStorage.removeItem("userData" + userID)
|
||||
NotificationsManager.success("API user Created");
|
||||
form.resetFields();
|
||||
localStorage.removeItem("userData" + userID);
|
||||
} catch (error: any) {
|
||||
const errorMessage = error.response?.data?.detail || error?.message || "Error creating the user"
|
||||
NotificationsManager.fromBackend(errorMessage)
|
||||
console.error("Error creating the user:", error)
|
||||
const errorMessage = error.response?.data?.detail || error?.message || "Error creating the user";
|
||||
NotificationsManager.fromBackend(errorMessage);
|
||||
console.error("Error creating the user:", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Modify the return statement to handle embedded mode
|
||||
if (isEmbedded) {
|
||||
|
|
@ -226,7 +226,7 @@ const Createuser: React.FC<CreateuserProps> = ({
|
|||
<Button htmlType="submit">Create User</Button>
|
||||
</div>
|
||||
</Form>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Original return for standalone mode
|
||||
|
|
@ -344,7 +344,7 @@ const Createuser: React.FC<CreateuserProps> = ({
|
|||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default Createuser
|
||||
export default Createuser;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import { Select, SelectItem, Text, Title } from "@tremor/react";
|
||||
import { ProxySettings, UserInfo } from "./user_dashboard";
|
||||
import { getProxyUISettings } from "./networking"
|
||||
import { getProxyUISettings } from "./networking";
|
||||
|
||||
interface DashboardTeamProps {
|
||||
teams: Object[] | null;
|
||||
|
|
@ -19,7 +19,7 @@ type TeamInterface = {
|
|||
team_id: null;
|
||||
team_alias: String;
|
||||
max_budget: number | null;
|
||||
}
|
||||
};
|
||||
|
||||
const DashboardTeam: React.FC<DashboardTeamProps> = ({
|
||||
teams,
|
||||
|
|
@ -29,15 +29,15 @@ const DashboardTeam: React.FC<DashboardTeamProps> = ({
|
|||
setProxySettings,
|
||||
userInfo,
|
||||
accessToken,
|
||||
setKeys
|
||||
setKeys,
|
||||
}) => {
|
||||
console.log(`userInfo: ${JSON.stringify(userInfo)}`)
|
||||
console.log(`userInfo: ${JSON.stringify(userInfo)}`);
|
||||
const defaultTeam: TeamInterface = {
|
||||
models: userInfo?.models || [],
|
||||
team_id: null,
|
||||
team_alias: "Default Team",
|
||||
max_budget: userInfo?.max_budget || null,
|
||||
}
|
||||
};
|
||||
|
||||
const getProxySettings = async () => {
|
||||
if (proxySettings === null && accessToken) {
|
||||
|
|
@ -53,8 +53,8 @@ const DashboardTeam: React.FC<DashboardTeamProps> = ({
|
|||
const [value, setValue] = useState(defaultTeam);
|
||||
|
||||
let updatedTeams;
|
||||
console.log(`userRole: ${userRole}`)
|
||||
console.log(`proxySettings: ${JSON.stringify(proxySettings)}`)
|
||||
console.log(`userRole: ${userRole}`);
|
||||
console.log(`proxySettings: ${JSON.stringify(proxySettings)}`);
|
||||
if (userRole === "App User") {
|
||||
// Non-Admin SSO users should only see their own team - they should not see "Default Team"
|
||||
updatedTeams = teams;
|
||||
|
|
@ -64,11 +64,10 @@ const DashboardTeam: React.FC<DashboardTeamProps> = ({
|
|||
updatedTeams = teams ? [...teams, defaultTeam] : [defaultTeam];
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div className="mt-5 mb-5">
|
||||
<Title>Select Team</Title>
|
||||
|
||||
|
||||
<Text>
|
||||
If you belong to multiple teams, this setting controls which team is used by default when creating new API Keys.
|
||||
</Text>
|
||||
|
|
|
|||
|
|
@ -3,72 +3,53 @@
|
|||
import React, { useState } from "react";
|
||||
import { Grid, Col, Icon } from "@tremor/react";
|
||||
import { Title } from "@tremor/react";
|
||||
import {
|
||||
Modal,
|
||||
message,
|
||||
} from "antd";
|
||||
import { Modal, message } from "antd";
|
||||
import { modelDeleteCall } from "./networking";
|
||||
import { TrashIcon } from "@heroicons/react/outline";
|
||||
import NotificationsManager from "./molecules/notifications_manager";
|
||||
interface DeleteModelProps {
|
||||
modelID: string;
|
||||
accessToken: string;
|
||||
callback?: ()=>void;
|
||||
modelID: string;
|
||||
accessToken: string;
|
||||
callback?: () => void;
|
||||
}
|
||||
|
||||
const DeleteModelButton: React.FC<DeleteModelProps> = ({
|
||||
modelID,
|
||||
accessToken,
|
||||
callback
|
||||
}) => {
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
const DeleteModelButton: React.FC<DeleteModelProps> = ({ modelID, accessToken, callback }) => {
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
|
||||
const handleDelete = async () => {
|
||||
try {
|
||||
NotificationsManager.info("Making API Call");
|
||||
setIsModalVisible(true);
|
||||
const response = await modelDeleteCall(accessToken, modelID);
|
||||
const handleDelete = async () => {
|
||||
try {
|
||||
NotificationsManager.info("Making API Call");
|
||||
setIsModalVisible(true);
|
||||
const response = await modelDeleteCall(accessToken, modelID);
|
||||
|
||||
console.log("model delete Response:", response);
|
||||
NotificationsManager.success(`Model ${modelID} deleted successfully`);
|
||||
setIsModalVisible(false);
|
||||
callback && setTimeout(callback, 4000) //added timeout of 4 seconds as deleted model is taking time to reflect in get models
|
||||
} catch (error) {
|
||||
console.error("Error deleting the model:", error);
|
||||
}
|
||||
};
|
||||
console.log("model delete Response:", response);
|
||||
NotificationsManager.success(`Model ${modelID} deleted successfully`);
|
||||
setIsModalVisible(false);
|
||||
callback && setTimeout(callback, 4000); //added timeout of 4 seconds as deleted model is taking time to reflect in get models
|
||||
} catch (error) {
|
||||
console.error("Error deleting the model:", error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Icon
|
||||
onClick={() => setIsModalVisible(true)}
|
||||
icon={TrashIcon}
|
||||
size="sm"
|
||||
/>
|
||||
return (
|
||||
<div>
|
||||
<Icon onClick={() => setIsModalVisible(true)} icon={TrashIcon} size="sm" />
|
||||
|
||||
<Modal
|
||||
open={isModalVisible}
|
||||
onOk={handleDelete}
|
||||
okType="danger"
|
||||
onCancel={() => setIsModalVisible(false)}
|
||||
>
|
||||
<Grid numItems={1} className="gap-2 w-full">
|
||||
|
||||
<Title>Delete Model</Title>
|
||||
<Col numColSpan={1}>
|
||||
<p>
|
||||
Are you sure you want to delete this model? This action is irreversible.
|
||||
</p>
|
||||
</Col>
|
||||
<Col numColSpan={1}>
|
||||
<p>
|
||||
Model ID: <b>{modelID}</b>
|
||||
</p>
|
||||
</Col>
|
||||
</Grid>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
<Modal open={isModalVisible} onOk={handleDelete} okType="danger" onCancel={() => setIsModalVisible(false)}>
|
||||
<Grid numItems={1} className="gap-2 w-full">
|
||||
<Title>Delete Model</Title>
|
||||
<Col numColSpan={1}>
|
||||
<p>Are you sure you want to delete this model? This action is irreversible.</p>
|
||||
</Col>
|
||||
<Col numColSpan={1}>
|
||||
<p>
|
||||
Model ID: <b>{modelID}</b>
|
||||
</p>
|
||||
</Col>
|
||||
</Grid>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeleteModelButton;
|
||||
export default DeleteModelButton;
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ const EditAutoRouterModal: React.FC<EditAutoRouterModalProps> = ({
|
|||
console.error("Error fetching model access groups:", error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const loadModels = async () => {
|
||||
if (!accessToken) return;
|
||||
try {
|
||||
|
|
@ -69,7 +69,7 @@ const EditAutoRouterModal: React.FC<EditAutoRouterModalProps> = ({
|
|||
// Parse the auto_router_config if it exists and is a string
|
||||
let parsedConfig = null;
|
||||
if (modelData.litellm_params?.auto_router_config) {
|
||||
if (typeof modelData.litellm_params.auto_router_config === 'string') {
|
||||
if (typeof modelData.litellm_params.auto_router_config === "string") {
|
||||
parsedConfig = JSON.parse(modelData.litellm_params.auto_router_config);
|
||||
} else {
|
||||
parsedConfig = modelData.litellm_params.auto_router_config;
|
||||
|
|
@ -81,16 +81,15 @@ const EditAutoRouterModal: React.FC<EditAutoRouterModalProps> = ({
|
|||
// Set form values
|
||||
form.setFieldsValue({
|
||||
auto_router_name: modelData.model_name,
|
||||
auto_router_default_model: modelData.litellm_params?.auto_router_default_model || '',
|
||||
auto_router_embedding_model: modelData.litellm_params?.auto_router_embedding_model || '',
|
||||
auto_router_default_model: modelData.litellm_params?.auto_router_default_model || "",
|
||||
auto_router_embedding_model: modelData.litellm_params?.auto_router_embedding_model || "",
|
||||
model_access_group: modelData.model_info?.access_groups || [],
|
||||
});
|
||||
|
||||
// Check if using custom models
|
||||
const allModelGroups = new Set(modelInfo.map(model => model.model_group));
|
||||
const allModelGroups = new Set(modelInfo.map((model) => model.model_group));
|
||||
setShowCustomDefaultModel(!allModelGroups.has(modelData.litellm_params?.auto_router_default_model));
|
||||
setShowCustomEmbeddingModel(!allModelGroups.has(modelData.litellm_params?.auto_router_embedding_model));
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error parsing auto router config:", error);
|
||||
NotificationsManager.fromBackend("Error loading auto router configuration");
|
||||
|
|
@ -142,7 +141,7 @@ const EditAutoRouterModal: React.FC<EditAutoRouterModalProps> = ({
|
|||
}
|
||||
};
|
||||
|
||||
const modelOptions = modelInfo.map(model => ({
|
||||
const modelOptions = modelInfo.map((model) => ({
|
||||
value: model.model_group,
|
||||
label: model.model_group,
|
||||
}));
|
||||
|
|
@ -168,11 +167,7 @@ const EditAutoRouterModal: React.FC<EditAutoRouterModalProps> = ({
|
|||
Edit the auto router configuration including routing logic, default models, and access settings.
|
||||
</Text>
|
||||
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
className="space-y-4"
|
||||
>
|
||||
<Form form={form} layout="vertical" className="space-y-4">
|
||||
{/* Auto Router Name */}
|
||||
<Form.Item
|
||||
label="Auto Router Name"
|
||||
|
|
@ -202,31 +197,21 @@ const EditAutoRouterModal: React.FC<EditAutoRouterModalProps> = ({
|
|||
<AntdSelect
|
||||
placeholder="Select a default model"
|
||||
onChange={(value) => {
|
||||
setShowCustomDefaultModel(value === 'custom');
|
||||
setShowCustomDefaultModel(value === "custom");
|
||||
}}
|
||||
options={[
|
||||
...modelOptions,
|
||||
{ value: 'custom', label: 'Enter custom model name' }
|
||||
]}
|
||||
options={[...modelOptions, { value: "custom", label: "Enter custom model name" }]}
|
||||
showSearch={true}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
|
||||
{/* Embedding Model */}
|
||||
<Form.Item
|
||||
label="Embedding Model"
|
||||
name="auto_router_embedding_model"
|
||||
>
|
||||
<Form.Item label="Embedding Model" name="auto_router_embedding_model">
|
||||
<AntdSelect
|
||||
placeholder="Select an embedding model (optional)"
|
||||
onChange={(value) => {
|
||||
setShowCustomEmbeddingModel(value === 'custom');
|
||||
setShowCustomEmbeddingModel(value === "custom");
|
||||
}}
|
||||
options={[
|
||||
...modelOptions,
|
||||
{ value: 'custom', label: 'Enter custom model name' }
|
||||
]}
|
||||
options={[...modelOptions, { value: "custom", label: "Enter custom model name" }]}
|
||||
showSearch={true}
|
||||
allowClear
|
||||
/>
|
||||
|
|
@ -244,10 +229,10 @@ const EditAutoRouterModal: React.FC<EditAutoRouterModalProps> = ({
|
|||
showSearch
|
||||
placeholder="Select existing groups or type to create new ones"
|
||||
optionFilterProp="children"
|
||||
tokenSeparators={[',']}
|
||||
tokenSeparators={[","]}
|
||||
options={modelAccessGroups.map((group) => ({
|
||||
value: group,
|
||||
label: group
|
||||
label: group,
|
||||
}))}
|
||||
maxTagCount="responsive"
|
||||
allowClear
|
||||
|
|
@ -260,4 +245,4 @@ const EditAutoRouterModal: React.FC<EditAutoRouterModalProps> = ({
|
|||
);
|
||||
};
|
||||
|
||||
export default EditAutoRouterModal;
|
||||
export default EditAutoRouterModal;
|
||||
|
|
|
|||
|
|
@ -11,7 +11,12 @@ interface EditModelModalProps {
|
|||
onSubmit: (data: FormData) => void;
|
||||
}
|
||||
|
||||
export const handleEditModelSubmit = async (formValues: Record<string, any>, accessToken: string | null, setEditModalVisible: (visible: boolean) => void, setSelectedModel: (model: any) => void) => {
|
||||
export const handleEditModelSubmit = async (
|
||||
formValues: Record<string, any>,
|
||||
accessToken: string | null,
|
||||
setEditModalVisible: (visible: boolean) => void,
|
||||
setSelectedModel: (model: any) => void,
|
||||
) => {
|
||||
// Call API to update team with teamId and values
|
||||
|
||||
console.log("handleEditSubmit:", formValues);
|
||||
|
|
@ -31,7 +36,6 @@ export const handleEditModelSubmit = async (formValues: Record<string, any>, acc
|
|||
formValues.output_cost_per_token = Number(formValues.output_cost_per_token) / 1_000_000;
|
||||
}
|
||||
|
||||
|
||||
for (const [key, value] of Object.entries(formValues)) {
|
||||
if (key !== "model_id") {
|
||||
// Empty string means user wants to null the value
|
||||
|
|
@ -40,24 +44,25 @@ export const handleEditModelSubmit = async (formValues: Record<string, any>, acc
|
|||
model_info_model_id = value === "" ? null : value;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let payload: {
|
||||
litellm_params: Record<string, any> | undefined;
|
||||
model_info: { id: any } | undefined;
|
||||
} = {
|
||||
litellm_params: Object.keys(newLiteLLMParams).length > 0 ? newLiteLLMParams : undefined,
|
||||
model_info: model_info_model_id !== undefined ? {
|
||||
id: model_info_model_id,
|
||||
} : undefined,
|
||||
model_info:
|
||||
model_info_model_id !== undefined
|
||||
? {
|
||||
id: model_info_model_id,
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
|
||||
console.log("handleEditSubmit payload:", payload);
|
||||
|
||||
try {
|
||||
let newModelValue = await modelUpdateCall(accessToken, payload);
|
||||
NotificationsManager.success(
|
||||
"Model updated successfully, restart server to see updates"
|
||||
);
|
||||
NotificationsManager.success("Model updated successfully, restart server to see updates");
|
||||
|
||||
setEditModalVisible(false);
|
||||
setSelectedModel(null);
|
||||
|
|
@ -66,12 +71,7 @@ export const handleEditModelSubmit = async (formValues: Record<string, any>, acc
|
|||
}
|
||||
};
|
||||
|
||||
const EditModelModal: React.FC<EditModelModalProps> = ({
|
||||
visible,
|
||||
onCancel,
|
||||
model,
|
||||
onSubmit,
|
||||
}) => {
|
||||
const EditModelModal: React.FC<EditModelModalProps> = ({ visible, onCancel, model, onSubmit }) => {
|
||||
const [form] = Form.useForm();
|
||||
let litellm_params_to_edit: Record<string, any> = {};
|
||||
let model_name = "";
|
||||
|
|
@ -79,8 +79,12 @@ const EditModelModal: React.FC<EditModelModalProps> = ({
|
|||
if (model) {
|
||||
litellm_params_to_edit = {
|
||||
...model.litellm_params,
|
||||
input_cost_per_token: model.litellm_params?.input_cost_per_token ? (model.litellm_params.input_cost_per_token * 1_000_000) : undefined,
|
||||
output_cost_per_token: model.litellm_params?.output_cost_per_token ? (model.litellm_params.output_cost_per_token * 1_000_000) : undefined,
|
||||
input_cost_per_token: model.litellm_params?.input_cost_per_token
|
||||
? model.litellm_params.input_cost_per_token * 1_000_000
|
||||
: undefined,
|
||||
output_cost_per_token: model.litellm_params?.output_cost_per_token
|
||||
? model.litellm_params.output_cost_per_token * 1_000_000
|
||||
: undefined,
|
||||
};
|
||||
model_name = model.model_name;
|
||||
let model_info = model.model_info;
|
||||
|
|
@ -97,8 +101,12 @@ const EditModelModal: React.FC<EditModelModalProps> = ({
|
|||
.then((values) => {
|
||||
const submissionValues = {
|
||||
...values,
|
||||
input_cost_per_token: values.input_cost_per_token ? Number(values.input_cost_per_token) / 1_000_000 : undefined,
|
||||
output_cost_per_token: values.output_cost_per_token ? Number(values.output_cost_per_token) / 1_000_000 : undefined,
|
||||
input_cost_per_token: values.input_cost_per_token
|
||||
? Number(values.input_cost_per_token) / 1_000_000
|
||||
: undefined,
|
||||
output_cost_per_token: values.output_cost_per_token
|
||||
? Number(values.output_cost_per_token) / 1_000_000
|
||||
: undefined,
|
||||
};
|
||||
onSubmit(submissionValues);
|
||||
form.resetFields();
|
||||
|
|
@ -153,11 +161,7 @@ const EditModelModal: React.FC<EditModelModalProps> = ({
|
|||
<Form.Item className="mt-8" label="model" name="model">
|
||||
<TextInput />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="organization"
|
||||
name="organization"
|
||||
tooltip="OpenAI Organization ID"
|
||||
>
|
||||
<Form.Item label="organization" name="organization" tooltip="OpenAI Organization ID">
|
||||
<TextInput />
|
||||
</Form.Item>
|
||||
|
||||
|
|
@ -197,7 +201,6 @@ const EditModelModal: React.FC<EditModelModalProps> = ({
|
|||
<InputNumber min={0} step={1} />
|
||||
</Form.Item>
|
||||
|
||||
|
||||
<Form.Item label="model_id" name="model_id" hidden={true}></Form.Item>
|
||||
</>
|
||||
<div style={{ textAlign: "right", marginTop: "10px" }}>
|
||||
|
|
@ -208,4 +211,4 @@ const EditModelModal: React.FC<EditModelModalProps> = ({
|
|||
);
|
||||
};
|
||||
|
||||
export default EditModelModal;
|
||||
export default EditModelModal;
|
||||
|
|
|
|||
|
|
@ -1,25 +1,7 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogPanel,
|
||||
TextInput,
|
||||
Button,
|
||||
Select,
|
||||
SelectItem,
|
||||
Text,
|
||||
Title,
|
||||
Subtitle,
|
||||
} from '@tremor/react';
|
||||
import { useEffect, useState } from "react";
|
||||
import { Dialog, DialogPanel, TextInput, Button, Select, SelectItem, Text, Title, Subtitle } from "@tremor/react";
|
||||
|
||||
import {
|
||||
Button as Button2,
|
||||
Modal,
|
||||
Form,
|
||||
Input,
|
||||
Select as Select2,
|
||||
message,
|
||||
InputNumber,
|
||||
} from "antd";
|
||||
import { Button as Button2, Modal, Form, Input, Select as Select2, message, InputNumber } from "antd";
|
||||
|
||||
import NumericalInput from "./shared/numerical_input";
|
||||
import BudgetDurationDropdown from "./common_components/budget_duration_dropdown";
|
||||
|
|
@ -52,100 +34,78 @@ const EditUserModal: React.FC<EditUserModalProps> = ({ visible, possibleUIRoles,
|
|||
onCancel();
|
||||
};
|
||||
|
||||
|
||||
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal visible={visible} onCancel={handleCancel} footer={null} title={"Edit User " + user.user_id} width={1000}>
|
||||
<Form
|
||||
form={form}
|
||||
onFinish={handleEditSubmit}
|
||||
initialValues={user} // Pass initial values here
|
||||
labelCol={{ span: 8 }}
|
||||
wrapperCol={{ span: 16 }}
|
||||
labelAlign="left"
|
||||
>
|
||||
<>
|
||||
<Form.Item className="mt-8" label="User Email" tooltip="Email of the User" name="user_email">
|
||||
<TextInput />
|
||||
</Form.Item>
|
||||
|
||||
<Modal
|
||||
visible={visible}
|
||||
onCancel={handleCancel}
|
||||
footer={null}
|
||||
title={"Edit User " + user.user_id}
|
||||
width={1000}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
onFinish={handleEditSubmit}
|
||||
initialValues={user} // Pass initial values here
|
||||
labelCol={{ span: 8 }}
|
||||
wrapperCol={{ span: 16 }}
|
||||
labelAlign="left"
|
||||
>
|
||||
<>
|
||||
<Form.Item
|
||||
className="mt-8"
|
||||
label="User Email"
|
||||
tooltip="Email of the User"
|
||||
name="user_email">
|
||||
<TextInput />
|
||||
</Form.Item>
|
||||
<Form.Item label="user_id" name="user_id" hidden={true}>
|
||||
<TextInput />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="user_id"
|
||||
name="user_id"
|
||||
hidden={true}
|
||||
>
|
||||
<TextInput />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="User Role"
|
||||
name="user_role"
|
||||
>
|
||||
<Form.Item label="User Role" name="user_role">
|
||||
<Select2>
|
||||
{possibleUIRoles &&
|
||||
Object.entries(possibleUIRoles).map(([role, { ui_label, description }]) => (
|
||||
<SelectItem key={role} value={role} title={ui_label}>
|
||||
<div className='flex'>
|
||||
{ui_label} <p className="ml-2" style={{ color: "gray", fontSize: "12px" }}>{description}</p>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
{possibleUIRoles &&
|
||||
Object.entries(possibleUIRoles).map(([role, { ui_label, description }]) => (
|
||||
<SelectItem key={role} value={role} title={ui_label}>
|
||||
<div className="flex">
|
||||
{ui_label}{" "}
|
||||
<p className="ml-2" style={{ color: "gray", fontSize: "12px" }}>
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select2>
|
||||
</Form.Item>
|
||||
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="Spend (USD)"
|
||||
name="spend"
|
||||
tooltip="(float) - Spend of all LLM calls completed by this user"
|
||||
help="Across all keys (including keys with team_id)."
|
||||
>
|
||||
<InputNumber min={0} step={0.01} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="Spend (USD)"
|
||||
name="spend"
|
||||
tooltip="(float) - Spend of all LLM calls completed by this user"
|
||||
help="Across all keys (including keys with team_id)."
|
||||
>
|
||||
<InputNumber min={0} step={0.01} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="User Budget (USD)"
|
||||
name="max_budget"
|
||||
tooltip="(float) - Maximum budget of this user"
|
||||
help="Maximum budget of this user."
|
||||
>
|
||||
<NumericalInput min={0} step={0.01} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="User Budget (USD)"
|
||||
name="max_budget"
|
||||
tooltip="(float) - Maximum budget of this user"
|
||||
help="Maximum budget of this user."
|
||||
>
|
||||
<NumericalInput min={0} step={0.01} />
|
||||
</Form.Item>
|
||||
<Form.Item label="Reset Budget" name="budget_duration">
|
||||
<BudgetDurationDropdown />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="Reset Budget" name="budget_duration">
|
||||
<BudgetDurationDropdown />
|
||||
</Form.Item>
|
||||
|
||||
<div style={{ textAlign: "right", marginTop: "10px" }}>
|
||||
<Button2 htmlType="submit">Save</Button2>
|
||||
</div>
|
||||
|
||||
<div style={{ textAlign: "right", marginTop: "10px" }}>
|
||||
<Button2 htmlType="submit">Save</Button2>
|
||||
</div>
|
||||
<div style={{ textAlign: "right", marginTop: "10px" }}>
|
||||
<Button2 htmlType="submit">Save</Button2>
|
||||
</div>
|
||||
|
||||
<div style={{ textAlign: "right", marginTop: "10px" }}>
|
||||
<Button2 htmlType="submit">Save</Button2>
|
||||
</div>
|
||||
</>
|
||||
|
||||
</Form>
|
||||
|
||||
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditUserModal;
|
||||
export default EditUserModal;
|
||||
|
|
|
|||
|
|
@ -12,9 +12,7 @@ interface EmailEventSettingsProps {
|
|||
accessToken: string | null;
|
||||
}
|
||||
|
||||
const EmailEventSettings: React.FC<EmailEventSettingsProps> = ({
|
||||
accessToken,
|
||||
}) => {
|
||||
const EmailEventSettings: React.FC<EmailEventSettingsProps> = ({ accessToken }) => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [eventSettings, setEventSettings] = useState<EmailEventSetting[]>([]);
|
||||
|
||||
|
|
@ -39,8 +37,8 @@ const EmailEventSettings: React.FC<EmailEventSettingsProps> = ({
|
|||
};
|
||||
|
||||
const handleCheckboxChange = (event: EmailEvent, checked: boolean) => {
|
||||
const updatedSettings = eventSettings.map(setting =>
|
||||
setting.event === event ? { ...setting, enabled: checked } : setting
|
||||
const updatedSettings = eventSettings.map((setting) =>
|
||||
setting.event === event ? { ...setting, enabled: checked } : setting,
|
||||
);
|
||||
setEventSettings(updatedSettings);
|
||||
};
|
||||
|
|
@ -80,7 +78,10 @@ const EmailEventSettings: React.FC<EmailEventSettingsProps> = ({
|
|||
return "An email will be sent to the email address of the user when a new user is created";
|
||||
} else {
|
||||
// Handle any other event type from the API
|
||||
const words = event.split(/(?=[A-Z])/).join(' ').toLowerCase();
|
||||
const words = event
|
||||
.split(/(?=[A-Z])/)
|
||||
.join(" ")
|
||||
.toLowerCase();
|
||||
return `Receive an email notification when ${words}`;
|
||||
}
|
||||
};
|
||||
|
|
@ -88,28 +89,24 @@ const EmailEventSettings: React.FC<EmailEventSettingsProps> = ({
|
|||
return (
|
||||
<Card>
|
||||
<Title level={4}>Email Notifications</Title>
|
||||
<Text>
|
||||
Select which events should trigger email notifications.
|
||||
</Text>
|
||||
<Text>Select which events should trigger email notifications.</Text>
|
||||
<Divider />
|
||||
|
||||
{loading ? (
|
||||
<div style={{ textAlign: 'center', padding: '20px' }}>
|
||||
<div style={{ textAlign: "center", padding: "20px" }}>
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{eventSettings.map((setting) => (
|
||||
<div key={setting.event} className="flex items-center">
|
||||
<Checkbox
|
||||
<Checkbox
|
||||
checked={setting.enabled}
|
||||
onChange={(e) => handleCheckboxChange(setting.event, e.target.checked)}
|
||||
/>
|
||||
<div className="ml-3">
|
||||
<Text>{setting.event}</Text>
|
||||
<div className="text-sm text-gray-500 block">
|
||||
{getEventDescription(setting.event)}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 block">{getEventDescription(setting.event)}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
|
@ -117,17 +114,10 @@ const EmailEventSettings: React.FC<EmailEventSettingsProps> = ({
|
|||
)}
|
||||
|
||||
<div className="mt-6 flex space-x-4">
|
||||
<Button
|
||||
onClick={handleSaveSettings}
|
||||
disabled={loading}
|
||||
>
|
||||
<Button onClick={handleSaveSettings} disabled={loading}>
|
||||
Save Changes
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleResetSettings}
|
||||
variant="secondary"
|
||||
disabled={loading}
|
||||
>
|
||||
<Button onClick={handleResetSettings} variant="secondary" disabled={loading}>
|
||||
Reset to Defaults
|
||||
</Button>
|
||||
</div>
|
||||
|
|
@ -135,4 +125,4 @@ const EmailEventSettings: React.FC<EmailEventSettingsProps> = ({
|
|||
);
|
||||
};
|
||||
|
||||
export default EmailEventSettings;
|
||||
export default EmailEventSettings;
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
export { default as EmailEventSettings } from './email_event_settings';
|
||||
export * from './types';
|
||||
export { default as EmailEventSettings } from "./email_event_settings";
|
||||
export * from "./types";
|
||||
|
|
|
|||
|
|
@ -11,4 +11,4 @@ export interface EmailEventSettingsUpdateRequest {
|
|||
|
||||
export interface EmailEventSettingsResponse {
|
||||
settings: EmailEventSetting[];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue