mirror of
https://github.com/BerriAI/litellm.git
synced 2025-04-25 18:54:30 +00:00
build(ui/litellm_dashboard_v_2): allow app owner to create keys and view their keys
This commit is contained in:
parent
af8b35d556
commit
dda115fcb7
8 changed files with 249 additions and 81 deletions
|
@ -190,17 +190,27 @@ def run_server(
|
||||||
global feature_telemetry
|
global feature_telemetry
|
||||||
args = locals()
|
args = locals()
|
||||||
if local:
|
if local:
|
||||||
from proxy_server import app, save_worker_config, usage_telemetry
|
from proxy_server import app, save_worker_config, usage_telemetry, ProxyConfig
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
from .proxy_server import app, save_worker_config, usage_telemetry
|
from .proxy_server import (
|
||||||
|
app,
|
||||||
|
save_worker_config,
|
||||||
|
usage_telemetry,
|
||||||
|
ProxyConfig,
|
||||||
|
)
|
||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
if "litellm[proxy]" in str(e):
|
if "litellm[proxy]" in str(e):
|
||||||
# user is missing a proxy dependency, ask them to pip install litellm[proxy]
|
# user is missing a proxy dependency, ask them to pip install litellm[proxy]
|
||||||
raise e
|
raise e
|
||||||
else:
|
else:
|
||||||
# this is just a local/relative import error, user git cloned litellm
|
# this is just a local/relative import error, user git cloned litellm
|
||||||
from proxy_server import app, save_worker_config, usage_telemetry
|
from proxy_server import (
|
||||||
|
app,
|
||||||
|
save_worker_config,
|
||||||
|
usage_telemetry,
|
||||||
|
ProxyConfig,
|
||||||
|
)
|
||||||
feature_telemetry = usage_telemetry
|
feature_telemetry = usage_telemetry
|
||||||
if version == True:
|
if version == True:
|
||||||
pkg_version = importlib.metadata.version("litellm")
|
pkg_version = importlib.metadata.version("litellm")
|
||||||
|
@ -373,16 +383,16 @@ def run_server(
|
||||||
read from there and save it to os.env['DATABASE_URL']
|
read from there and save it to os.env['DATABASE_URL']
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
import yaml
|
import yaml, asyncio
|
||||||
except:
|
except:
|
||||||
raise ImportError(
|
raise ImportError(
|
||||||
"yaml needs to be imported. Run - `pip install 'litellm[proxy]'`"
|
"yaml needs to be imported. Run - `pip install 'litellm[proxy]'`"
|
||||||
)
|
)
|
||||||
|
|
||||||
if os.path.exists(config):
|
proxy_config = ProxyConfig()
|
||||||
with open(config, "r") as config_file:
|
_, _, general_settings = asyncio.run(
|
||||||
config = yaml.safe_load(config_file)
|
proxy_config.load_config(router=None, config_file_path=config)
|
||||||
general_settings = config.get("general_settings", {})
|
)
|
||||||
database_url = general_settings.get("database_url", None)
|
database_url = general_settings.get("database_url", None)
|
||||||
if database_url and database_url.startswith("os.environ/"):
|
if database_url and database_url.startswith("os.environ/"):
|
||||||
original_dir = os.getcwd()
|
original_dir = os.getcwd()
|
||||||
|
|
|
@ -515,11 +515,15 @@ async def user_api_key_auth(
|
||||||
)
|
)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(route.startswith("/key/") or route.startswith("/user/"))
|
(
|
||||||
or route.startswith("/model/")
|
route.startswith("/key/")
|
||||||
and not is_master_key_valid
|
or route.startswith("/user/")
|
||||||
and general_settings.get("allow_user_auth", False) != True
|
or route.startswith("/model/")
|
||||||
|
)
|
||||||
|
and (not is_master_key_valid)
|
||||||
|
and (not general_settings.get("allow_user_auth", False))
|
||||||
):
|
):
|
||||||
|
assert not general_settings.get("allow_user_auth", False)
|
||||||
if route == "/key/info":
|
if route == "/key/info":
|
||||||
# check if user can access this route
|
# check if user can access this route
|
||||||
query_params = request.query_params
|
query_params = request.query_params
|
||||||
|
@ -546,7 +550,7 @@ async def user_api_key_auth(
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
raise Exception(
|
raise Exception(
|
||||||
f"If master key is set, only master key can be used to generate, delete, update or get info for new keys/users"
|
f"only master key can be used to generate, delete, update or get info for new keys/users."
|
||||||
)
|
)
|
||||||
|
|
||||||
return UserAPIKeyAuth(api_key=api_key, **valid_token_dict)
|
return UserAPIKeyAuth(api_key=api_key, **valid_token_dict)
|
||||||
|
|
|
@ -2190,7 +2190,6 @@ def client(original_function):
|
||||||
result = original_function(*args, **kwargs)
|
result = original_function(*args, **kwargs)
|
||||||
end_time = datetime.datetime.now()
|
end_time = datetime.datetime.now()
|
||||||
if "stream" in kwargs and kwargs["stream"] == True:
|
if "stream" in kwargs and kwargs["stream"] == True:
|
||||||
# TODO: Add to cache for streaming
|
|
||||||
if (
|
if (
|
||||||
"complete_response" in kwargs
|
"complete_response" in kwargs
|
||||||
and kwargs["complete_response"] == True
|
and kwargs["complete_response"] == True
|
||||||
|
|
|
@ -1,22 +1,13 @@
|
||||||
import React from 'react';
|
import React from "react";
|
||||||
import CreateKey from "../components/create_key_button"
|
import Navbar from "../components/navbar";
|
||||||
import ViewKeyTable from "../components/view_key_table"
|
import UserDashboard from "../components/user_dashboard";
|
||||||
import Navbar from "../components/navbar"
|
|
||||||
import { Grid, Col } from "@tremor/react";
|
|
||||||
|
|
||||||
const CreateKeyPage = () => {
|
const CreateKeyPage = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='h-screen'>
|
<div className="flex min-h-screen flex-col items-center">
|
||||||
<Navbar/>
|
<Navbar />
|
||||||
<Grid numItems={1} className="gap-0 p-10 h-[75vh]">
|
<UserDashboard />
|
||||||
<Col numColSpan={1}>
|
|
||||||
<ViewKeyTable/>
|
|
||||||
<CreateKey/>
|
|
||||||
</Col>
|
|
||||||
</Grid>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default CreateKeyPage;
|
export default CreateKeyPage;
|
||||||
|
|
|
@ -1,21 +1,40 @@
|
||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import React from 'react';
|
import React, { use } from "react";
|
||||||
import { Button, TextInput } from '@tremor/react';
|
import { Button, TextInput } from "@tremor/react";
|
||||||
|
|
||||||
import { Card, Metric, Text } from "@tremor/react";
|
import { Card, Metric, Text } from "@tremor/react";
|
||||||
|
import { createKeyCall } from "./networking";
|
||||||
|
// Define the props type
|
||||||
|
interface CreateKeyProps {
|
||||||
|
userID: string;
|
||||||
|
accessToken: string;
|
||||||
|
proxyBaseUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default function CreateKey() {
|
const CreateKey: React.FC<CreateKeyProps> = ({
|
||||||
|
userID,
|
||||||
|
accessToken,
|
||||||
|
proxyBaseUrl,
|
||||||
|
}) => {
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
console.log('Hello World');
|
console.log("Hello World");
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// <Card className="max-w-xs mx-auto" decoration="top" decorationColor="indigo">
|
<Button
|
||||||
// <Text className='mb-4'>Key Name</Text>
|
className="mx-auto"
|
||||||
// <TextInput className='mb-4' placeholder="My test key"></TextInput>
|
onClick={() =>
|
||||||
|
createKeyCall(
|
||||||
// </Card>
|
(proxyBaseUrl = proxyBaseUrl),
|
||||||
<Button className="mx-auto" onClick={handleClick}>+ Create New Key</Button>
|
(accessToken = accessToken),
|
||||||
|
(userID = userID)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
+ Create New Key
|
||||||
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default CreateKey;
|
||||||
|
|
65
ui/litellm-dashboard/src/components/networking.tsx
Normal file
65
ui/litellm-dashboard/src/components/networking.tsx
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
/**
|
||||||
|
* Helper file for calls being made to proxy
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const createKeyCall = async (
|
||||||
|
proxyBaseUrl: String,
|
||||||
|
accessToken: String,
|
||||||
|
userID: String
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${proxyBaseUrl}/key/generate`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
team_id: "core-infra-4",
|
||||||
|
max_budget: 10,
|
||||||
|
user_id: userID,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Network response was not ok");
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
console.log(data);
|
||||||
|
// Handle success - you might want to update some state or UI based on the created key
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to create key:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const userInfoCall = async (
|
||||||
|
proxyBaseUrl: String,
|
||||||
|
accessToken: String,
|
||||||
|
userID: String
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`${proxyBaseUrl}/user/info?user_id=${userID}`,
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Network response was not ok");
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
console.log(data);
|
||||||
|
return data;
|
||||||
|
// Handle success - you might want to update some state or UI based on the created key
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to create key:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
42
ui/litellm-dashboard/src/components/user_dashboard.tsx
Normal file
42
ui/litellm-dashboard/src/components/user_dashboard.tsx
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
"use client";
|
||||||
|
import React from "react";
|
||||||
|
import { Grid, Col, Card, Text } from "@tremor/react";
|
||||||
|
import CreateKey from "./create_key_button";
|
||||||
|
import ViewKeyTable from "./view_key_table";
|
||||||
|
import { useSearchParams } from "next/navigation";
|
||||||
|
|
||||||
|
export default function UserDashboard() {
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const userID = searchParams.get("userID");
|
||||||
|
const accessToken = searchParams.get("accessToken");
|
||||||
|
const proxyBaseUrl = searchParams.get("proxyBaseUrl");
|
||||||
|
|
||||||
|
if (userID == null || accessToken == null || proxyBaseUrl == null) {
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
className="max-w-xs mx-auto"
|
||||||
|
decoration="top"
|
||||||
|
decorationColor="indigo"
|
||||||
|
>
|
||||||
|
<Text>Login to create/delete keys</Text>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid numItems={1} className="gap-0 p-10 h-[75vh]">
|
||||||
|
<Col numColSpan={1}>
|
||||||
|
<ViewKeyTable
|
||||||
|
userID={userID}
|
||||||
|
accessToken={accessToken}
|
||||||
|
proxyBaseUrl={proxyBaseUrl}
|
||||||
|
/>
|
||||||
|
<CreateKey
|
||||||
|
userID={userID}
|
||||||
|
accessToken={accessToken}
|
||||||
|
proxyBaseUrl={proxyBaseUrl}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,4 +1,6 @@
|
||||||
'use client';
|
"use client";
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { userInfoCall } from "./networking";
|
||||||
import { StatusOnlineIcon } from "@heroicons/react/outline";
|
import { StatusOnlineIcon } from "@heroicons/react/outline";
|
||||||
import {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
|
@ -13,69 +15,105 @@ import {
|
||||||
Title,
|
Title,
|
||||||
} from "@tremor/react";
|
} from "@tremor/react";
|
||||||
|
|
||||||
|
// Define the props type
|
||||||
|
interface ViewKeyTableProps {
|
||||||
|
userID: string;
|
||||||
|
accessToken: string;
|
||||||
|
proxyBaseUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
const data = [
|
const data = [
|
||||||
{
|
{
|
||||||
key_alias: "my test key",
|
key_alias: "my test key",
|
||||||
key_name: "sk-...hd74",
|
key_name: "sk-...hd74",
|
||||||
spend: 23.0,
|
spend: 23.0,
|
||||||
expires: "active",
|
expires: "active",
|
||||||
token: "23902dwojd90"
|
token: "23902dwojd90",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key_alias: "my test key",
|
key_alias: "my test key",
|
||||||
key_name: "sk-...hd74",
|
key_name: "sk-...hd74",
|
||||||
spend: 23.0,
|
spend: 23.0,
|
||||||
expires: "active",
|
expires: "active",
|
||||||
token: "23902dwojd90"
|
token: "23902dwojd90",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key_alias: "my test key",
|
key_alias: "my test key",
|
||||||
key_name: "sk-...hd74",
|
key_name: "sk-...hd74",
|
||||||
spend: 23.0,
|
spend: 23.0,
|
||||||
expires: "active",
|
expires: "active",
|
||||||
token: "23902dwojd90"
|
token: "23902dwojd90",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key_alias: "my test key",
|
key_alias: "my test key",
|
||||||
key_name: "sk-...hd74",
|
key_name: "sk-...hd74",
|
||||||
spend: 23.0,
|
spend: 23.0,
|
||||||
expires: "active",
|
expires: "active",
|
||||||
token: "23902dwojd90"
|
token: "23902dwojd90",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function ViewKeyTable() {
|
const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
|
||||||
|
userID,
|
||||||
return (
|
accessToken,
|
||||||
<Card className="flex-auto overflow-y-auto max-h-[50vh] mb-4">
|
proxyBaseUrl,
|
||||||
<Title>API Keys</Title>
|
}) => {
|
||||||
<Table className="mt-5">
|
const [data, setData] = useState(null); // State to store the data from the API
|
||||||
<TableHead>
|
|
||||||
<TableRow>
|
useEffect(() => {
|
||||||
<TableHeaderCell>Alias</TableHeaderCell>
|
const fetchData = async () => {
|
||||||
<TableHeaderCell>Secret Key</TableHeaderCell>
|
try {
|
||||||
<TableHeaderCell>Spend</TableHeaderCell>
|
const response = await userInfoCall(
|
||||||
<TableHeaderCell>Status</TableHeaderCell>
|
(proxyBaseUrl = proxyBaseUrl),
|
||||||
</TableRow>
|
(accessToken = accessToken),
|
||||||
</TableHead>
|
(userID = userID)
|
||||||
<TableBody>
|
);
|
||||||
{data.map((item) => (
|
setData(response["keys"]); // Update state with the fetched data
|
||||||
<TableRow key={item.token}>
|
} catch (error) {
|
||||||
<TableCell>{item.key_alias}</TableCell>
|
console.error("There was an error fetching the data", error);
|
||||||
<TableCell>
|
// Optionally, update your UI to reflect the error state
|
||||||
<Text>{item.key_name}</Text>
|
}
|
||||||
</TableCell>
|
};
|
||||||
<TableCell>
|
|
||||||
<Text>{item.spend}</Text>
|
fetchData(); // Call the async function to fetch data
|
||||||
</TableCell>
|
}, []); // Empty dependency array
|
||||||
<TableCell>
|
|
||||||
<Badge color="emerald" icon={StatusOnlineIcon}>
|
if (data == null) {
|
||||||
{item.expires}
|
return;
|
||||||
</Badge>
|
}
|
||||||
</TableCell>
|
return (
|
||||||
|
<Card className="flex-auto overflow-y-auto max-h-[50vh] mb-4">
|
||||||
|
<Title>API Keys</Title>
|
||||||
|
<Table className="mt-5">
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<TableHeaderCell>Alias</TableHeaderCell>
|
||||||
|
<TableHeaderCell>Secret Key</TableHeaderCell>
|
||||||
|
<TableHeaderCell>Spend</TableHeaderCell>
|
||||||
|
<TableHeaderCell>Status</TableHeaderCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
</TableHead>
|
||||||
</TableBody>
|
<TableBody>
|
||||||
</Table>
|
{data.map((item) => (
|
||||||
</Card>
|
<TableRow key={item.token}>
|
||||||
)};
|
<TableCell>{item.key_alias}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Text>{item.key_name}</Text>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Text>{item.spend}</Text>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge color="emerald" icon={StatusOnlineIcon}>
|
||||||
|
{item.expires}
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ViewKeyTable;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue