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
|
||||
args = locals()
|
||||
if local:
|
||||
from proxy_server import app, save_worker_config, usage_telemetry
|
||||
from proxy_server import app, save_worker_config, usage_telemetry, ProxyConfig
|
||||
else:
|
||||
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:
|
||||
if "litellm[proxy]" in str(e):
|
||||
# user is missing a proxy dependency, ask them to pip install litellm[proxy]
|
||||
raise e
|
||||
else:
|
||||
# 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
|
||||
if version == True:
|
||||
pkg_version = importlib.metadata.version("litellm")
|
||||
|
@ -373,16 +383,16 @@ def run_server(
|
|||
read from there and save it to os.env['DATABASE_URL']
|
||||
"""
|
||||
try:
|
||||
import yaml
|
||||
import yaml, asyncio
|
||||
except:
|
||||
raise ImportError(
|
||||
"yaml needs to be imported. Run - `pip install 'litellm[proxy]'`"
|
||||
)
|
||||
|
||||
if os.path.exists(config):
|
||||
with open(config, "r") as config_file:
|
||||
config = yaml.safe_load(config_file)
|
||||
general_settings = config.get("general_settings", {})
|
||||
proxy_config = ProxyConfig()
|
||||
_, _, general_settings = asyncio.run(
|
||||
proxy_config.load_config(router=None, config_file_path=config)
|
||||
)
|
||||
database_url = general_settings.get("database_url", None)
|
||||
if database_url and database_url.startswith("os.environ/"):
|
||||
original_dir = os.getcwd()
|
||||
|
|
|
@ -515,11 +515,15 @@ async def user_api_key_auth(
|
|||
)
|
||||
|
||||
if (
|
||||
(route.startswith("/key/") or route.startswith("/user/"))
|
||||
(
|
||||
route.startswith("/key/")
|
||||
or route.startswith("/user/")
|
||||
or route.startswith("/model/")
|
||||
and not is_master_key_valid
|
||||
and general_settings.get("allow_user_auth", False) != True
|
||||
)
|
||||
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":
|
||||
# check if user can access this route
|
||||
query_params = request.query_params
|
||||
|
@ -546,7 +550,7 @@ async def user_api_key_auth(
|
|||
pass
|
||||
else:
|
||||
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)
|
||||
|
|
|
@ -2190,7 +2190,6 @@ def client(original_function):
|
|||
result = original_function(*args, **kwargs)
|
||||
end_time = datetime.datetime.now()
|
||||
if "stream" in kwargs and kwargs["stream"] == True:
|
||||
# TODO: Add to cache for streaming
|
||||
if (
|
||||
"complete_response" in kwargs
|
||||
and kwargs["complete_response"] == True
|
||||
|
|
|
@ -1,20 +1,11 @@
|
|||
import React from 'react';
|
||||
import CreateKey from "../components/create_key_button"
|
||||
import ViewKeyTable from "../components/view_key_table"
|
||||
import Navbar from "../components/navbar"
|
||||
import { Grid, Col } from "@tremor/react";
|
||||
|
||||
import React from "react";
|
||||
import Navbar from "../components/navbar";
|
||||
import UserDashboard from "../components/user_dashboard";
|
||||
const CreateKeyPage = () => {
|
||||
|
||||
return (
|
||||
<div className='h-screen'>
|
||||
<div className="flex min-h-screen flex-col items-center">
|
||||
<Navbar />
|
||||
<Grid numItems={1} className="gap-0 p-10 h-[75vh]">
|
||||
<Col numColSpan={1}>
|
||||
<ViewKeyTable/>
|
||||
<CreateKey/>
|
||||
</Col>
|
||||
</Grid>
|
||||
<UserDashboard />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,21 +1,40 @@
|
|||
'use client';
|
||||
"use client";
|
||||
|
||||
import React from 'react';
|
||||
import { Button, TextInput } from '@tremor/react';
|
||||
import React, { use } from "react";
|
||||
import { Button, TextInput } 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 = () => {
|
||||
console.log('Hello World');
|
||||
console.log("Hello World");
|
||||
};
|
||||
|
||||
return (
|
||||
// <Card className="max-w-xs mx-auto" decoration="top" decorationColor="indigo">
|
||||
// <Text className='mb-4'>Key Name</Text>
|
||||
// <TextInput className='mb-4' placeholder="My test key"></TextInput>
|
||||
|
||||
// </Card>
|
||||
<Button className="mx-auto" onClick={handleClick}>+ Create New Key</Button>
|
||||
);
|
||||
<Button
|
||||
className="mx-auto"
|
||||
onClick={() =>
|
||||
createKeyCall(
|
||||
(proxyBaseUrl = proxyBaseUrl),
|
||||
(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 {
|
||||
Badge,
|
||||
|
@ -13,39 +15,72 @@ import {
|
|||
Title,
|
||||
} from "@tremor/react";
|
||||
|
||||
// Define the props type
|
||||
interface ViewKeyTableProps {
|
||||
userID: string;
|
||||
accessToken: string;
|
||||
proxyBaseUrl: string;
|
||||
}
|
||||
|
||||
const data = [
|
||||
{
|
||||
key_alias: "my test key",
|
||||
key_name: "sk-...hd74",
|
||||
spend: 23.0,
|
||||
expires: "active",
|
||||
token: "23902dwojd90"
|
||||
token: "23902dwojd90",
|
||||
},
|
||||
{
|
||||
key_alias: "my test key",
|
||||
key_name: "sk-...hd74",
|
||||
spend: 23.0,
|
||||
expires: "active",
|
||||
token: "23902dwojd90"
|
||||
token: "23902dwojd90",
|
||||
},
|
||||
{
|
||||
key_alias: "my test key",
|
||||
key_name: "sk-...hd74",
|
||||
spend: 23.0,
|
||||
expires: "active",
|
||||
token: "23902dwojd90"
|
||||
token: "23902dwojd90",
|
||||
},
|
||||
{
|
||||
key_alias: "my test key",
|
||||
key_name: "sk-...hd74",
|
||||
spend: 23.0,
|
||||
expires: "active",
|
||||
token: "23902dwojd90"
|
||||
token: "23902dwojd90",
|
||||
},
|
||||
];
|
||||
|
||||
export default function ViewKeyTable() {
|
||||
const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
|
||||
userID,
|
||||
accessToken,
|
||||
proxyBaseUrl,
|
||||
}) => {
|
||||
const [data, setData] = useState(null); // State to store the data from the API
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const response = await userInfoCall(
|
||||
(proxyBaseUrl = proxyBaseUrl),
|
||||
(accessToken = accessToken),
|
||||
(userID = userID)
|
||||
);
|
||||
setData(response["keys"]); // Update state with the fetched data
|
||||
} catch (error) {
|
||||
console.error("There was an error fetching the data", error);
|
||||
// Optionally, update your UI to reflect the error state
|
||||
}
|
||||
};
|
||||
|
||||
fetchData(); // Call the async function to fetch data
|
||||
}, []); // Empty dependency array
|
||||
|
||||
if (data == null) {
|
||||
return;
|
||||
}
|
||||
return (
|
||||
<Card className="flex-auto overflow-y-auto max-h-[50vh] mb-4">
|
||||
<Title>API Keys</Title>
|
||||
|
@ -78,4 +113,7 @@ export default function ViewKeyTable() {
|
|||
</TableBody>
|
||||
</Table>
|
||||
</Card>
|
||||
)};
|
||||
);
|
||||
};
|
||||
|
||||
export default ViewKeyTable;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue