added and ran prettier autoformatter

This commit is contained in:
= 2025-10-04 18:19:48 -07:00
parent e58c2c5c3e
commit 5197268a58
298 changed files with 17336 additions and 20204 deletions

View file

@ -0,0 +1,11 @@
node_modules
.next
.out
dist
build
.coverage
.vercel
.turbo
.next-static
*.min.js
coverage/

View file

@ -0,0 +1,7 @@
{
"semi": true,
"singleQuote": false,
"tabWidth": 2,
"printWidth": 120,
"trailingComma": "all"
}

View file

@ -1,7 +0,0 @@
{
"semi": false,
"tabWidth": 2,
"printWidth": 120,
"trailingComma": "all",
"jsxBracketSameLine": false
}

View file

@ -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;

View file

@ -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"
},

View file

@ -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",

View file

@ -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 {

View file

@ -19,7 +19,5 @@ export default function PublicModelHub() {
* populate navbar
*
*/
return (
<PublicModelHubPage accessToken={accessToken} />
);
return <PublicModelHubPage accessToken={accessToken} />;
}

View file

@ -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} />;
}

View file

@ -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" />

View file

@ -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>
)
);
}

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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",
},
});
});
});
});

View file

@ -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;

View file

@ -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>
);
};

View file

@ -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;

View file

@ -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" },
];

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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 = &quot;example-name&quot;</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 = &quot;example-name&quot;</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;

View file

@ -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);
}
};
};

View file

@ -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);
}
};

View file

@ -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;

View file

@ -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;

View file

@ -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"

View file

@ -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;

View file

@ -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>
);
};

View file

@ -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>

View file

@ -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) {

View file

@ -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>

View file

@ -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>
)
);
}

View file

@ -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;

View file

@ -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>
)
}
);
};

View file

@ -1 +1 @@
export { Tooltip } from './Tooltip';
export { Tooltip } from "./Tooltip";

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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;

View file

@ -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 &quot;user&quot; role by default. All users will be added to each selected team.
<Text type="secondary" style={{ fontSize: "12px" }}>
Users will be added with &quot;user&quot; 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;

View file

@ -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;

View file

@ -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>
);
};
};

View file

@ -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",
},
};

View file

@ -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;

View file

@ -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;

View file

@ -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

View file

@ -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;
};

View file

@ -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;

View file

@ -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;
};
};

View file

@ -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>

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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
);
};
};

View file

@ -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;

View file

@ -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;

View file

@ -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
}
}

View file

@ -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 [];
}
}
}

View file

@ -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);

View file

@ -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
}
}
}

View file

@ -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) {

View file

@ -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);
}
};
};

View file

@ -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
}
}
}

View file

@ -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;
};

View file

@ -81,4 +81,4 @@ export interface MultimodalContent {
type: "input_text" | "input_image";
text?: string;
image_url?: string;
}
}

View file

@ -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;

View file

@ -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}

View file

@ -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&apos;ll receive a notification with the new key. The old key will be deactivated after a brief grace period.
When rotation occurs, you&apos;ll receive a notification with the new key. The old key will be deactivated
after a brief grace period.
</div>
)}
</div>

View file

@ -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&apos;s how your current aliases would look in the config:
</Text>
<Text className="text-gray-600 mb-4">Here&apos;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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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&apos;re overallocating {limitTypeLower} (Team/Key Limits checked at runtime).
<div style={{ fontSize: "11px", color: "#6b7280", marginTop: "2px" }}>
Best effort throughput - no error if we&apos;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&apos;re overallocating {limitTypeLower} (also checks model-specific limits)
<div style={{ fontSize: "11px", color: "#6b7280", marginTop: "2px" }}>
Guaranteed throughput - raise an error if we&apos;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;

View file

@ -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 };

View file

@ -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;

View file

@ -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>
);
})}

View file

@ -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;

View file

@ -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;

View file

@ -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);
};

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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>

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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";

View file

@ -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