build(ui/litellm_dashboard_v_2): allow app owner to create keys and view their keys

This commit is contained in:
Krrish Dholakia 2024-01-27 13:48:35 -08:00
parent af8b35d556
commit dda115fcb7
8 changed files with 249 additions and 81 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

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

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

View file

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