diff --git a/litellm/proxy/_experimental/out/404.html b/litellm/proxy/_experimental/out/404.html
index 1ec6cd9a4..c57eb5193 100644
--- a/litellm/proxy/_experimental/out/404.html
+++ b/litellm/proxy/_experimental/out/404.html
@@ -1 +1 @@
-
404: This page could not be found.Create Next App404
This page could not be found.
\ No newline at end of file
+404: This page could not be found.🚅 LiteLLM404
This page could not be found.
\ No newline at end of file
diff --git a/litellm/proxy/_experimental/out/_next/static/chunks/app/layout-4d667c133e03c98b.js b/litellm/proxy/_experimental/out/_next/static/chunks/app/layout-ea657eeec2abf062.js
similarity index 60%
rename from litellm/proxy/_experimental/out/_next/static/chunks/app/layout-4d667c133e03c98b.js
rename to litellm/proxy/_experimental/out/_next/static/chunks/app/layout-ea657eeec2abf062.js
index e261adc05..fe5260feb 100644
--- a/litellm/proxy/_experimental/out/_next/static/chunks/app/layout-4d667c133e03c98b.js
+++ b/litellm/proxy/_experimental/out/_next/static/chunks/app/layout-ea657eeec2abf062.js
@@ -1 +1 @@
-(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[185],{87421:function(n,e,t){Promise.resolve().then(t.t.bind(t,99646,23)),Promise.resolve().then(t.t.bind(t,63385,23))},63385:function(){},99646:function(n){n.exports={style:{fontFamily:"'__Inter_c23dc8', '__Inter_Fallback_c23dc8'",fontStyle:"normal"},className:"__className_c23dc8"}}},function(n){n.O(0,[971,69,744],function(){return n(n.s=87421)}),_N_E=n.O()}]);
\ No newline at end of file
+(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[185],{11837:function(n,e,t){Promise.resolve().then(t.t.bind(t,99646,23)),Promise.resolve().then(t.t.bind(t,63385,23))},63385:function(){},99646:function(n){n.exports={style:{fontFamily:"'__Inter_c23dc8', '__Inter_Fallback_c23dc8'",fontStyle:"normal"},className:"__className_c23dc8"}}},function(n){n.O(0,[971,69,744],function(){return n(n.s=11837)}),_N_E=n.O()}]);
\ No newline at end of file
diff --git a/litellm/proxy/_experimental/out/_next/static/chunks/app/page-7f03ccc8529ada97.js b/litellm/proxy/_experimental/out/_next/static/chunks/app/page-7f03ccc8529ada97.js
new file mode 100644
index 000000000..c8c53fcee
--- /dev/null
+++ b/litellm/proxy/_experimental/out/_next/static/chunks/app/page-7f03ccc8529ada97.js
@@ -0,0 +1 @@
+(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[931],{27376:function(e,t,l){Promise.resolve().then(l.bind(l,27680))},27680:function(e,t,l){"use strict";l.r(t),l.d(t,{default:function(){return B}});var s=l(3827),r=l(64090),a=l(80588);let n=async(e,t,l)=>{try{if(console.log("Form Values in keyCreateCall:",l),l.description&&(l.metadata||(l.metadata={}),l.metadata.description=l.description,delete l.description,l.metadata=JSON.stringify(l.metadata)),l.metadata){console.log("formValues.metadata:",l.metadata);try{l.metadata=JSON.parse(l.metadata)}catch(e){throw a.ZP.error("Failed to parse metadata: "+e),Error("Failed to parse metadata: "+e)}}console.log("Form Values after check:",l);let s=await fetch("/key/generate",{method:"POST",headers:{Authorization:"Bearer ".concat(e),"Content-Type":"application/json"},body:JSON.stringify({user_id:t,...l})});if(!s.ok){let e=await s.text();throw a.ZP.error("Failed to create key: "+e),console.error("Error response from the server:",e),Error("Network response was not ok")}let r=await s.json();return console.log("API Response:",r),r}catch(e){throw console.error("Failed to create key:",e),e}},o=async(e,t)=>{try{console.log("in keyDeleteCall:",t),a.ZP.info("Making key delete request");let l=await fetch("/key/delete",{method:"POST",headers:{Authorization:"Bearer ".concat(e),"Content-Type":"application/json"},body:JSON.stringify({keys:[t]})});if(!l.ok){let e=await l.text();throw a.ZP.error("Failed to delete key: "+e),Error("Network response was not ok")}let s=await l.json();return console.log(s),a.ZP.success("API Key Deleted"),s}catch(e){throw console.error("Failed to create key:",e),e}},i=async(e,t,l)=>{try{let s="/user/info";"App Owner"==l&&(s="".concat(s,"/?user_id=").concat(t)),a.ZP.info("Requesting user data");let r=await fetch(s,{method:"GET",headers:{Authorization:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!r.ok){let e=await r.text();throw a.ZP.error(e),Error("Network response was not ok")}let n=await r.json();return a.ZP.info("Received user data"),n}catch(e){throw console.error("Failed to create key:",e),e}},c=async(e,t)=>{try{let l="/spend/logs";console.log("in keySpendLogsCall:",l);let s=await fetch("".concat(l,"/?api_key=").concat(t),{method:"GET",headers:{Authorization:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!s.ok){let e=await s.text();throw a.ZP.error(e),Error("Network response was not ok")}let r=await s.json();return console.log(r),r}catch(e){throw console.error("Failed to create key:",e),e}};var d=l(10384),h=l(46453),u=l(2179),m=l(71801),x=l(96776),j=l(2902),p=l(77171),y=l(29714),Z=l(88707),g=l(1861);let{Option:f}=x.default;var k=e=>{let{userID:t,userRole:l,accessToken:o,data:i,setData:c}=e,[x]=j.Z.useForm(),[f,k]=(0,r.useState)(!1),[w,b]=(0,r.useState)(null),S=()=>{k(!1),x.resetFields()},N=()=>{k(!1),b(null),x.resetFields()},_=async e=>{try{a.ZP.info("Making API Call"),e.models&&""!==e.models.trim()?e.models=e.models.split(",").map(e=>e.trim()):e.models=[],k(!0);let l=await n(o,t,e);c(e=>e?[...e,l]:[l]),b(l.key),a.ZP.success("API Key Created"),x.resetFields()}catch(e){console.error("Error creating the key:",e)}};return(0,s.jsxs)("div",{children:[(0,s.jsx)(u.Z,{className:"mx-auto",onClick:()=>k(!0),children:"+ Create New Key"}),(0,s.jsx)(p.Z,{title:"Create Key",visible:f,width:800,footer:null,onOk:S,onCancel:N,children:(0,s.jsxs)(j.Z,{form:x,onFinish:_,labelCol:{span:6},wrapperCol:{span:16},labelAlign:"left",children:["App Owner"===l||"Admin"===l?(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(j.Z.Item,{label:"Key Name",name:"key_alias",children:(0,s.jsx)(y.Z,{})}),(0,s.jsx)(j.Z.Item,{label:"Team ID",name:"team_id",children:(0,s.jsx)(y.Z,{placeholder:"ai_team"})}),(0,s.jsx)(j.Z.Item,{label:"Models (Comma Separated). Eg: gpt-3.5-turbo,gpt-4",name:"models",children:(0,s.jsx)(y.Z,{placeholder:"gpt-4,gpt-3.5-turbo"})}),(0,s.jsx)(j.Z.Item,{label:"Max Budget (USD)",name:"max_budget",children:(0,s.jsx)(Z.Z,{step:.01,precision:2,width:200})}),(0,s.jsx)(j.Z.Item,{label:"Duration (eg: 30s, 30h, 30d)",name:"duration",children:(0,s.jsx)(y.Z,{})}),(0,s.jsx)(j.Z.Item,{label:"Metadata",name:"metadata",children:(0,s.jsx)(y.Z.TextArea,{rows:4,placeholder:"Enter metadata as JSON"})})]}):(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(j.Z.Item,{label:"Key Name",name:"key_alias",children:(0,s.jsx)(y.Z,{})}),(0,s.jsx)(j.Z.Item,{label:"Team ID (Contact Group)",name:"team_id",children:(0,s.jsx)(y.Z,{placeholder:"ai_team"})}),(0,s.jsx)(j.Z.Item,{label:"Description",name:"description",children:(0,s.jsx)(y.Z.TextArea,{placeholder:"Enter description",rows:4})})]}),(0,s.jsx)("div",{style:{textAlign:"right",marginTop:"10px"},children:(0,s.jsx)(g.ZP,{htmlType:"submit",children:"Create Key"})})]})}),w&&(0,s.jsx)(p.Z,{title:"Save your key",visible:f,onOk:S,onCancel:N,footer:null,children:(0,s.jsxs)(h.Z,{numItems:1,className:"gap-2 w-full",children:[(0,s.jsx)(d.Z,{numColSpan:1,children:(0,s.jsxs)("p",{children:["Please save this secret key somewhere safe and accessible. For security reasons, ",(0,s.jsx)("b",{children:"you will not be able to view it again"})," ","through your LiteLLM account. If you lose this secret key, you will need to generate a new one."]})}),(0,s.jsx)(d.Z,{numColSpan:1,children:null!=w?(0,s.jsxs)(m.Z,{children:["API Key: ",w]}):(0,s.jsx)(m.Z,{children:"Key being created, this might take 30s"})})]})})]})},w=l(33393),b=l(13810),S=l(61244),N=l(10827),_=l(3851),D=l(2044),v=l(64167),C=l(74480),I=l(7178),E=l(42440),T=l(9853),A=l(67989),O=l(56863),P=e=>{let{token:t,accessToken:l,keySpend:a,keyBudget:n,keyName:o}=e,[i,d]=(0,r.useState)(!1),[h,m]=(0,r.useState)(null),[x,j]=(0,r.useState)(null),y=async()=>{try{if(null==l||null==t)return;console.log("accessToken: ".concat(l,"; token: ").concat(t));let e=await c(l,t);console.log("Response:",e);let s=Object.values(e).reduce((e,t)=>{let l=new Date(t.startTime),s=new Intl.DateTimeFormat("en-US",{day:"2-digit",month:"short"}).format(l);return e[s]=(e[s]||0)+t.spend,e},{}),r=Object.entries(s);r.sort((e,t)=>{let[l]=e,[s]=t,r=new Date(l),a=new Date(s);return r.getTime()-a.getTime()});let a=Object.fromEntries(r);console.log(a);let n=Object.values(e).reduce((e,t)=>{let l=t.user;return e[l]=(e[l]||0)+t.spend,e},{});console.log(s),console.log(n);let o=[];for(let[e,t]of Object.entries(a))o.push({day:e,spend:t});let i=Object.entries(n).sort((e,t)=>t[1]-e[1]).slice(0,5).map(e=>{let[t,l]=e;return{name:t,value:l}});m(o),j(i),console.log("arrayBarChart:",o)}catch(e){console.error("There was an error fetching the data",e)}};return t?(0,s.jsxs)("div",{children:[(0,s.jsx)(u.Z,{className:"mx-auto",onClick:()=>{console.log("Show Modal triggered"),d(!0),y()},children:"View Spend Report"}),(0,s.jsxs)(p.Z,{visible:i,width:1e3,onOk:()=>{d(!1)},onCancel:()=>{d(!1)},footer:null,children:[(0,s.jsxs)(E.Z,{style:{textAlign:"left"},children:["Key Name: ",o]}),(0,s.jsxs)(O.Z,{children:["Monthly Spend $",a]}),(0,s.jsx)(b.Z,{className:"mt-6 mb-6",children:h&&(0,s.jsx)(T.Z,{className:"mt-6",data:h,colors:["green"],index:"day",categories:["spend"],yAxisWidth:48})}),(0,s.jsx)(E.Z,{className:"mt-6",children:"Top 5 Users Spend (USD)"}),(0,s.jsx)(b.Z,{className:"mb-6",children:x&&(0,s.jsx)(A.Z,{className:"mt-6",data:x,color:"teal"})})]})]}):null},F=e=>{let{userID:t,accessToken:l,data:a,setData:n}=e,[i,c]=(0,r.useState)(!1),d=async e=>{if(null!=a)try{await o(l,e);let t=a.filter(t=>t.token!==e);n(t)}catch(e){console.error("Error deleting the key:",e)}};if(null!=a)return console.log("RERENDER TRIGGERED"),(0,s.jsxs)(b.Z,{className:"w-full mx-auto flex-auto overflow-y-auto max-h-[50vh] mb-4",children:[(0,s.jsx)(E.Z,{children:"API Keys"}),(0,s.jsxs)(N.Z,{className:"mt-5",children:[(0,s.jsx)(v.Z,{children:(0,s.jsxs)(I.Z,{children:[(0,s.jsx)(C.Z,{children:"Key Alias"}),(0,s.jsx)(C.Z,{children:"Secret Key"}),(0,s.jsx)(C.Z,{children:"Spend (USD)"}),(0,s.jsx)(C.Z,{children:"Key Budget (USD)"}),(0,s.jsx)(C.Z,{children:"Team ID"}),(0,s.jsx)(C.Z,{children:"Metadata"}),(0,s.jsx)(C.Z,{children:"Expires"})]})}),(0,s.jsx)(_.Z,{children:a.map(e=>(console.log(e),"litellm-dashboard"===e.team_id)?null:(0,s.jsxs)(I.Z,{children:[(0,s.jsx)(D.Z,{children:null!=e.key_alias?(0,s.jsx)(m.Z,{children:e.key_alias}):(0,s.jsx)(m.Z,{children:"Not Set"})}),(0,s.jsx)(D.Z,{children:(0,s.jsx)(m.Z,{children:e.key_name})}),(0,s.jsx)(D.Z,{children:(0,s.jsx)(m.Z,{children:e.spend})}),(0,s.jsx)(D.Z,{children:null!=e.max_budget?(0,s.jsx)(m.Z,{children:e.max_budget}):(0,s.jsx)(m.Z,{children:"Unlimited Budget"})}),(0,s.jsx)(D.Z,{children:(0,s.jsx)(m.Z,{children:e.team_id})}),(0,s.jsx)(D.Z,{children:(0,s.jsx)(m.Z,{children:JSON.stringify(e.metadata)})}),(0,s.jsx)(D.Z,{children:null!=e.expires?(0,s.jsx)(m.Z,{children:e.expires}):(0,s.jsx)(m.Z,{children:"Never expires"})}),(0,s.jsx)(D.Z,{children:(0,s.jsx)(S.Z,{onClick:()=>d(e.token),icon:w.Z,size:"sm"})}),(0,s.jsx)(D.Z,{children:(0,s.jsx)(P,{token:e.token,accessToken:l,keySpend:e.spend,keyBudget:e.max_budget,keyName:e.key_name})})]},e.token))})]})]})},R=e=>{let{userID:t,userSpendData:l}=e;console.log("User SpendData:",l);let r=null==l?void 0:l.spend,a=(null==l?void 0:l.max_budget)||null,n=null!==a?"$".concat(a," limit"):"No limit";return"$".concat(r," / ").concat(n),(0,s.jsx)(s.Fragment,{children:(0,s.jsxs)(b.Z,{className:"mx-auto mb-4",children:[(0,s.jsxs)(O.Z,{children:["$",r]}),(0,s.jsxs)(E.Z,{children:["/ ",n]})]})})},U=l(8792),K=e=>{let{userID:t,userRole:l,userEmail:r}=e;return console.log("User ID:",t),console.log("userEmail:",r),(0,s.jsxs)("nav",{className:"left-0 right-0 top-0 flex justify-between items-center h-12 mb-4",children:[(0,s.jsx)("div",{className:"text-left mx-4 my-2 absolute top-0 left-0",children:(0,s.jsx)("div",{className:"flex flex-col items-center",children:(0,s.jsx)(U.default,{href:"/",children:(0,s.jsx)("button",{className:"text-gray-800 text-2xl px-4 py-1 rounded text-center",children:"\uD83D\uDE85 LiteLLM"})})})}),(0,s.jsx)("div",{className:"text-right mx-4 my-2 absolute top-0 right-0",children:(0,s.jsxs)(u.Z,{variant:"secondary",children:[r,(0,s.jsxs)("p",{children:["Role: ",l]}),(0,s.jsxs)("p",{children:["ID: ",t]})]})})]})},L=l(47907),M=l(37963);console.log("isLocal:",!1);var B=()=>{let[e,t]=(0,r.useState)(null),[l,a]=(0,r.useState)(null),n=(0,L.useSearchParams)(),o=n.get("userID");n.get("viewSpend");let c=n.get("token"),[u,m]=(0,r.useState)(null),[x,j]=(0,r.useState)(null),[p,y]=(0,r.useState)(null);if((0,r.useEffect)(()=>{if(c){let e=(0,M.o)(c);if(e){if(console.log("Decoded token:",e),console.log("Decoded key:",e.key),m(e.key),e.user_role){let t=function(e){if(!e)return"Undefined Role";switch(console.log("Received user role: ".concat(e)),e.toLowerCase()){case"app_owner":case"demo_app_owner":return"App Owner";case"app_admin":return"Admin";case"app_user":return"App User";default:return"Unknown Role"}}(e.user_role);console.log("Decoded user_role:",t),j(t)}else console.log("User role not defined");e.user_email?y(e.user_email):console.log("User Email is not set ".concat(e))}}o&&u&&x&&!e&&(async()=>{try{let e=await i(u,o,x);a(e.user_info),t(e.keys)}catch(e){console.error("There was an error fetching the data",e)}})()},[o,c,u,e]),null==o||null==c){let e="/sso/key/generate";return console.log("Full URL:",e),window.location.href=e,null}return null==u?null:(null==x&&j("App Owner"),(0,s.jsxs)("div",{children:[(0,s.jsx)(K,{userID:o,userRole:x,userEmail:p}),(0,s.jsx)(h.Z,{numItems:1,className:"gap-0 p-10 h-[75vh] w-full",children:(0,s.jsxs)(d.Z,{numColSpan:1,children:[(0,s.jsx)(R,{userID:o,userSpendData:l}),(0,s.jsx)(F,{userID:o,accessToken:u,data:e,setData:t}),(0,s.jsx)(k,{userID:o,userRole:x,accessToken:u,data:e,setData:t})]})})]}))}}},function(e){e.O(0,[787,971,69,744],function(){return e(e.s=27376)}),_N_E=e.O()}]);
\ No newline at end of file
diff --git a/litellm/proxy/_experimental/out/_next/static/chunks/app/page-992f4cdd1053ee86.js b/litellm/proxy/_experimental/out/_next/static/chunks/app/page-992f4cdd1053ee86.js
deleted file mode 100644
index cd4ccb43c..000000000
--- a/litellm/proxy/_experimental/out/_next/static/chunks/app/page-992f4cdd1053ee86.js
+++ /dev/null
@@ -1 +0,0 @@
-(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[931],{88102:function(e,t,l){Promise.resolve().then(l.bind(l,27680))},27680:function(e,t,l){"use strict";l.r(t),l.d(t,{default:function(){return M}});var r=l(3827),s=l(64090),n=l(80588);let a=async(e,t,l)=>{try{if(console.log("Form Values in keyCreateCall:",l),l.description&&(l.metadata||(l.metadata={}),l.metadata.description=l.description,delete l.description,l.metadata=JSON.stringify(l.metadata)),l.metadata){console.log("formValues.metadata:",l.metadata);try{l.metadata=JSON.parse(l.metadata)}catch(e){throw n.ZP.error("Failed to parse metadata: "+e),Error("Failed to parse metadata: "+e)}}console.log("Form Values after check:",l);let r=await fetch("/key/generate",{method:"POST",headers:{Authorization:"Bearer ".concat(e),"Content-Type":"application/json"},body:JSON.stringify({user_id:t,...l})});if(!r.ok){let e=await r.text();throw n.ZP.error("Failed to create key: "+e),console.error("Error response from the server:",e),Error("Network response was not ok")}let s=await r.json();return console.log("API Response:",s),s}catch(e){throw console.error("Failed to create key:",e),e}},o=async(e,t)=>{try{console.log("in keyDeleteCall:",t);let l=await fetch("/key/delete",{method:"POST",headers:{Authorization:"Bearer ".concat(e),"Content-Type":"application/json"},body:JSON.stringify({keys:[t]})});if(!l.ok){let e=await l.text();throw n.ZP.error("Failed to delete key: "+e),Error("Network response was not ok")}let r=await l.json();return console.log(r),n.ZP.success("API Key Deleted"),r}catch(e){throw console.error("Failed to create key:",e),e}},i=async(e,t)=>{try{let l="/user/info";console.log("in userInfoCall:",l);let r=await fetch("".concat(l,"/?user_id=").concat(t),{method:"GET",headers:{Authorization:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!r.ok){let e=await r.text();throw n.ZP.error(e),Error("Network response was not ok")}let s=await r.json();return console.log(s),s}catch(e){throw console.error("Failed to create key:",e),e}},c=async(e,t)=>{try{let l="/spend/logs";console.log("in keySpendLogsCall:",l);let r=await fetch("".concat(l,"/?api_key=").concat(t),{method:"GET",headers:{Authorization:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!r.ok){let e=await r.text();throw n.ZP.error(e),Error("Network response was not ok")}let s=await r.json();return console.log(s),s}catch(e){throw console.error("Failed to create key:",e),e}};var d=l(10384),h=l(46453),u=l(2179),m=l(71801),x=l(96776),j=l(2902),p=l(77171),y=l(29714),Z=l(88707),g=l(1861);let{Option:f}=x.default;var w=e=>{let{userID:t,userRole:l,accessToken:o,data:i,setData:c}=e,[x]=j.Z.useForm(),[f,w]=(0,s.useState)(!1),[k,b]=(0,s.useState)(null),N=()=>{w(!1),x.resetFields()},S=()=>{w(!1),b(null),x.resetFields()},_=async e=>{try{n.ZP.info("Making API Call"),e.models&&""!==e.models.trim()?e.models=e.models.split(",").map(e=>e.trim()):e.models=[],w(!0);let l=await a(o,t,e);c(e=>e?[...e,l]:[l]),b(l.key),n.ZP.success("API Key Created"),x.resetFields()}catch(e){console.error("Error creating the key:",e)}};return(0,r.jsxs)("div",{children:[(0,r.jsx)(u.Z,{className:"mx-auto",onClick:()=>w(!0),children:"+ Create New Key"}),(0,r.jsx)(p.Z,{title:"Create Key",visible:f,width:800,footer:null,onOk:N,onCancel:S,children:(0,r.jsxs)(j.Z,{form:x,onFinish:_,labelCol:{span:6},wrapperCol:{span:16},labelAlign:"left",children:["App Owner"===l||"Admin"===l?(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(j.Z.Item,{label:"Key Name",name:"key_alias",children:(0,r.jsx)(y.Z,{})}),(0,r.jsx)(j.Z.Item,{label:"Team ID",name:"team_id",children:(0,r.jsx)(y.Z,{placeholder:"ai_team"})}),(0,r.jsx)(j.Z.Item,{label:"Models (Comma Separated). Eg: gpt-3.5-turbo,gpt-4",name:"models",children:(0,r.jsx)(y.Z,{placeholder:"gpt-4,gpt-3.5-turbo"})}),(0,r.jsx)(j.Z.Item,{label:"Max Budget (USD)",name:"max_budget",children:(0,r.jsx)(Z.Z,{step:.01,precision:2,width:200})}),(0,r.jsx)(j.Z.Item,{label:"Duration (eg: 30s, 30h, 30d)",name:"duration",children:(0,r.jsx)(y.Z,{})}),(0,r.jsx)(j.Z.Item,{label:"Metadata",name:"metadata",children:(0,r.jsx)(y.Z.TextArea,{rows:4,placeholder:"Enter metadata as JSON"})})]}):(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(j.Z.Item,{label:"Key Name",name:"key_alias",children:(0,r.jsx)(y.Z,{})}),(0,r.jsx)(j.Z.Item,{label:"Team ID (Contact Group)",name:"team_id",children:(0,r.jsx)(y.Z,{placeholder:"ai_team"})}),(0,r.jsx)(j.Z.Item,{label:"Description",name:"description",children:(0,r.jsx)(y.Z.TextArea,{placeholder:"Enter description",rows:4})})]}),(0,r.jsx)("div",{style:{textAlign:"right",marginTop:"10px"},children:(0,r.jsx)(g.ZP,{htmlType:"submit",children:"Create Key"})})]})}),k&&(0,r.jsx)(p.Z,{title:"Save your key",visible:f,onOk:N,onCancel:S,footer:null,children:(0,r.jsxs)(h.Z,{numItems:1,className:"gap-2 w-full",children:[(0,r.jsx)(d.Z,{numColSpan:1,children:(0,r.jsxs)("p",{children:["Please save this secret key somewhere safe and accessible. For security reasons, ",(0,r.jsx)("b",{children:"you will not be able to view it again"})," ","through your LiteLLM account. If you lose this secret key, you will need to generate a new one."]})}),(0,r.jsx)(d.Z,{numColSpan:1,children:null!=k?(0,r.jsxs)(m.Z,{children:["API Key: ",k]}):(0,r.jsx)(m.Z,{children:"Key being created, this might take 30s"})})]})})]})},k=l(33393),b=l(13810),N=l(61244),S=l(10827),_=l(3851),D=l(2044),C=l(64167),v=l(74480),I=l(7178),E=l(42440),T=l(9853),A=l(67989),O=l(56863),F=e=>{let{token:t,accessToken:l,keySpend:n,keyBudget:a,keyName:o}=e,[i,d]=(0,s.useState)(!1),[h,m]=(0,s.useState)(null),[x,j]=(0,s.useState)(null),y=async()=>{try{if(null==l||null==t)return;let e=await c(l,t);console.log("Response:",e);let r=Object.values(e).reduce((e,t)=>{let l=new Date(t.startTime),r=new Intl.DateTimeFormat("en-US",{day:"2-digit",month:"short"}).format(l);return e[r]=(e[r]||0)+t.spend,e},{}),s=Object.entries(r);s.sort((e,t)=>{let[l]=e,[r]=t,s=new Date(l),n=new Date(r);return s.getTime()-n.getTime()});let n=Object.fromEntries(s);console.log(n);let a=Object.values(e).reduce((e,t)=>{let l=t.user;return e[l]=(e[l]||0)+t.spend,e},{});console.log(r),console.log(a);let o=[];for(let[e,t]of Object.entries(n))o.push({day:e,spend:t});let i=Object.entries(a).sort((e,t)=>t[1]-e[1]).slice(0,5).map(e=>{let[t,l]=e;return{name:t,value:l}});m(o),j(i),console.log("arrayBarChart:",o)}catch(e){console.error("There was an error fetching the data",e)}};return((0,s.useEffect)(()=>{y()},[t]),t)?(0,r.jsxs)("div",{children:[(0,r.jsx)(u.Z,{className:"mx-auto",onClick:()=>{d(!0)},children:"View Spend Report"}),(0,r.jsxs)(p.Z,{visible:i,width:1e3,onOk:()=>{d(!1)},onCancel:()=>{d(!1)},footer:null,children:[(0,r.jsxs)(E.Z,{style:{textAlign:"left"},children:["Key Name: ",o]}),(0,r.jsxs)(O.Z,{children:["Monthly Spend $",n]}),(0,r.jsx)(b.Z,{className:"mt-6 mb-6",children:h&&(0,r.jsx)(T.Z,{className:"mt-6",data:h,colors:["green"],index:"day",categories:["spend"],yAxisWidth:48})}),(0,r.jsx)(E.Z,{className:"mt-6",children:"Top 5 Users Spend (USD)"}),(0,r.jsx)(b.Z,{className:"mb-6",children:x&&(0,r.jsx)(A.Z,{className:"mt-6",data:x,color:"teal"})})]})]}):null},P=e=>{let{userID:t,accessToken:l,data:s,setData:n}=e,a=async e=>{if(null!=s)try{await o(l,e);let t=s.filter(t=>t.token!==e);n(t)}catch(e){console.error("Error deleting the key:",e)}};if(null!=s)return console.log("RERENDER TRIGGERED"),(0,r.jsxs)(b.Z,{className:"w-full mx-auto flex-auto overflow-y-auto max-h-[50vh] mb-4",children:[(0,r.jsx)(E.Z,{children:"API Keys"}),(0,r.jsxs)(S.Z,{className:"mt-5",children:[(0,r.jsx)(C.Z,{children:(0,r.jsxs)(I.Z,{children:[(0,r.jsx)(v.Z,{children:"Key Alias"}),(0,r.jsx)(v.Z,{children:"Secret Key"}),(0,r.jsx)(v.Z,{children:"Spend (USD)"}),(0,r.jsx)(v.Z,{children:"Key Budget (USD)"}),(0,r.jsx)(v.Z,{children:"Team ID"}),(0,r.jsx)(v.Z,{children:"Metadata"}),(0,r.jsx)(v.Z,{children:"Expires"})]})}),(0,r.jsx)(_.Z,{children:s.map(e=>(console.log(e),"litellm-dashboard"===e.team_id)?null:(0,r.jsxs)(I.Z,{children:[(0,r.jsx)(D.Z,{children:null!=e.key_alias?(0,r.jsx)(m.Z,{children:e.key_alias}):(0,r.jsx)(m.Z,{children:"Not Set"})}),(0,r.jsx)(D.Z,{children:(0,r.jsx)(m.Z,{children:e.key_name})}),(0,r.jsx)(D.Z,{children:(0,r.jsx)(m.Z,{children:e.spend})}),(0,r.jsx)(D.Z,{children:null!=e.max_budget?(0,r.jsx)(m.Z,{children:e.max_budget}):(0,r.jsx)(m.Z,{children:"Unlimited Budget"})}),(0,r.jsx)(D.Z,{children:(0,r.jsx)(m.Z,{children:e.team_id})}),(0,r.jsx)(D.Z,{children:(0,r.jsx)(m.Z,{children:JSON.stringify(e.metadata)})}),(0,r.jsx)(D.Z,{children:null!=e.expires?(0,r.jsx)(m.Z,{children:e.expires}):(0,r.jsx)(m.Z,{children:"Never expires"})}),(0,r.jsx)(D.Z,{children:(0,r.jsx)(N.Z,{onClick:()=>a(e.token),icon:k.Z,size:"sm"})}),(0,r.jsx)(D.Z,{children:(0,r.jsx)(F,{token:e.token,accessToken:l,keySpend:e.spend,keyBudget:e.max_budget,keyName:e.key_name})})]},e.token))})]})]})},R=e=>{let{userID:t,userSpendData:l}=e;console.log("User SpendData:",l);let s=null==l?void 0:l.spend,n=(null==l?void 0:l.max_budget)||null,a=null!==n?"$".concat(n," limit"):"No limit";return"$".concat(s," / ").concat(a),(0,r.jsx)(r.Fragment,{children:(0,r.jsxs)(b.Z,{className:"mx-auto mb-4",children:[(0,r.jsxs)(O.Z,{children:["$",s]}),(0,r.jsxs)(E.Z,{children:["/ ",a]})]})})},K=l(8792),U=e=>{let{userID:t,userRole:l}=e;return console.log("User ID:",t),(0,r.jsxs)("nav",{className:"left-0 right-0 top-0 flex justify-between items-center h-12 mb-4",children:[(0,r.jsx)("div",{className:"text-left mx-4 my-2 absolute top-0 left-0",children:(0,r.jsx)("div",{className:"flex flex-col items-center",children:(0,r.jsx)(K.default,{href:"/",children:(0,r.jsx)("button",{className:"text-gray-800 text-2xl px-4 py-1 rounded text-center",children:"\uD83D\uDE85 LiteLLM"})})})}),(0,r.jsx)("div",{className:"text-right mx-4 my-2 absolute top-0 right-0",children:(0,r.jsxs)(u.Z,{variant:"secondary",children:[t,(0,r.jsxs)("p",{children:["Role: ",l]})]})})]})},B=l(47907),L=l(37963),M=()=>{let[e,t]=(0,s.useState)(null),[l,n]=(0,s.useState)(null),a=(0,B.useSearchParams)(),o=a.get("userID");a.get("viewSpend");let c=a.get("token"),[u,m]=(0,s.useState)(null),[x,j]=(0,s.useState)(null);if((0,s.useEffect)(()=>{if(c){let e=(0,L.o)(c);if(e){if(console.log("Decoded token:",e),console.log("Decoded key:",e.key),m(e.key),e.user_role){let t=function(e){if(!e)return"Undefined Role";switch(e.toLowerCase()){case"app_owner":return"App Owner";case"demo_app_owner":return"AppOwner";case"admin":return"Admin";case"app_user":return"App User";default:return"Unknown Role"}}(e.user_role);console.log("Decoded user_role:",t),j(t)}else console.log("User role not defined")}}o&&u&&!e&&(async()=>{try{let e=await i(u,o);console.log("Response:",e),n(e.user_info),t(e.keys)}catch(e){console.error("There was an error fetching the data",e)}})()},[o,c,u,e]),null==o||null==c){let e="/sso/key/generate";return console.log("Full URL:",e),window.location.href=e,null}return null==u?null:(null==x&&j("App Owner"),(0,r.jsxs)("div",{children:[(0,r.jsx)(U,{userID:o,userRole:x}),(0,r.jsx)(h.Z,{numItems:1,className:"gap-0 p-10 h-[75vh] w-full",children:(0,r.jsxs)(d.Z,{numColSpan:1,children:[(0,r.jsx)(R,{userID:o,userSpendData:l}),(0,r.jsx)(P,{userID:o,accessToken:u,data:e,setData:t}),(0,r.jsx)(w,{userID:o,userRole:x,accessToken:u,data:e,setData:t})]})})]}))}}},function(e){e.O(0,[787,971,69,744],function(){return e(e.s=88102)}),_N_E=e.O()}]);
\ No newline at end of file
diff --git a/ui/litellm-dashboard/out/_next/static/chunks/main-app-9b4fb13a7db53edf.js b/litellm/proxy/_experimental/out/_next/static/chunks/main-app-096338c8e1915716.js
similarity index 54%
rename from ui/litellm-dashboard/out/_next/static/chunks/main-app-9b4fb13a7db53edf.js
rename to litellm/proxy/_experimental/out/_next/static/chunks/main-app-096338c8e1915716.js
index 440df3cb3..421ae3e2c 100644
--- a/ui/litellm-dashboard/out/_next/static/chunks/main-app-9b4fb13a7db53edf.js
+++ b/litellm/proxy/_experimental/out/_next/static/chunks/main-app-096338c8e1915716.js
@@ -1 +1 @@
-(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[744],{32028:function(e,n,t){Promise.resolve().then(t.t.bind(t,47690,23)),Promise.resolve().then(t.t.bind(t,48955,23)),Promise.resolve().then(t.t.bind(t,5613,23)),Promise.resolve().then(t.t.bind(t,11902,23)),Promise.resolve().then(t.t.bind(t,31778,23)),Promise.resolve().then(t.t.bind(t,77831,23))}},function(e){var n=function(n){return e(e.s=n)};e.O(0,[971,69],function(){return n(35317),n(32028)}),_N_E=e.O()}]);
\ No newline at end of file
+(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[744],{70377:function(e,n,t){Promise.resolve().then(t.t.bind(t,47690,23)),Promise.resolve().then(t.t.bind(t,48955,23)),Promise.resolve().then(t.t.bind(t,5613,23)),Promise.resolve().then(t.t.bind(t,11902,23)),Promise.resolve().then(t.t.bind(t,31778,23)),Promise.resolve().then(t.t.bind(t,77831,23))}},function(e){var n=function(n){return e(e.s=n)};e.O(0,[971,69],function(){return n(35317),n(70377)}),_N_E=e.O()}]);
\ No newline at end of file
diff --git a/litellm/proxy/_experimental/out/_next/static/chunks/main-app-9b4fb13a7db53edf.js b/litellm/proxy/_experimental/out/_next/static/chunks/main-app-9b4fb13a7db53edf.js
deleted file mode 100644
index 440df3cb3..000000000
--- a/litellm/proxy/_experimental/out/_next/static/chunks/main-app-9b4fb13a7db53edf.js
+++ /dev/null
@@ -1 +0,0 @@
-(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[744],{32028:function(e,n,t){Promise.resolve().then(t.t.bind(t,47690,23)),Promise.resolve().then(t.t.bind(t,48955,23)),Promise.resolve().then(t.t.bind(t,5613,23)),Promise.resolve().then(t.t.bind(t,11902,23)),Promise.resolve().then(t.t.bind(t,31778,23)),Promise.resolve().then(t.t.bind(t,77831,23))}},function(e){var n=function(n){return e(e.s=n)};e.O(0,[971,69],function(){return n(35317),n(32028)}),_N_E=e.O()}]);
\ No newline at end of file
diff --git a/litellm/proxy/_experimental/out/_next/static/lGjwnJSGwBqa476jHHI8W/_buildManifest.js b/litellm/proxy/_experimental/out/_next/static/p5gDwQBbgW8D3Uz3lgoZg/_buildManifest.js
similarity index 100%
rename from litellm/proxy/_experimental/out/_next/static/lGjwnJSGwBqa476jHHI8W/_buildManifest.js
rename to litellm/proxy/_experimental/out/_next/static/p5gDwQBbgW8D3Uz3lgoZg/_buildManifest.js
diff --git a/litellm/proxy/_experimental/out/_next/static/lGjwnJSGwBqa476jHHI8W/_ssgManifest.js b/litellm/proxy/_experimental/out/_next/static/p5gDwQBbgW8D3Uz3lgoZg/_ssgManifest.js
similarity index 100%
rename from litellm/proxy/_experimental/out/_next/static/lGjwnJSGwBqa476jHHI8W/_ssgManifest.js
rename to litellm/proxy/_experimental/out/_next/static/p5gDwQBbgW8D3Uz3lgoZg/_ssgManifest.js
diff --git a/litellm/proxy/_experimental/out/index.html b/litellm/proxy/_experimental/out/index.html
index f0fb6f14c..9537fb723 100644
--- a/litellm/proxy/_experimental/out/index.html
+++ b/litellm/proxy/_experimental/out/index.html
@@ -1 +1 @@
-Create Next AppLoading...
\ No newline at end of file
+🚅 LiteLLMLoading...
\ No newline at end of file
diff --git a/litellm/proxy/_experimental/out/index.txt b/litellm/proxy/_experimental/out/index.txt
index db2a9c631..f48954f2c 100644
--- a/litellm/proxy/_experimental/out/index.txt
+++ b/litellm/proxy/_experimental/out/index.txt
@@ -1,7 +1,7 @@
2:"$Sreact.suspense"
-3:I[27680,["787","static/chunks/787-5bb33960644f5c7c.js","931","static/chunks/app/page-992f4cdd1053ee86.js"],""]
+3:I[27680,["787","static/chunks/787-5bb33960644f5c7c.js","931","static/chunks/app/page-7f03ccc8529ada97.js"],""]
4:I[5613,[],""]
5:I[31778,[],""]
-0:["lGjwnJSGwBqa476jHHI8W",[[["",{"children":["__PAGE__",{}]},"$undefined","$undefined",true],["",{"children":["__PAGE__",{},["$L1",["$","$2",null,{"fallback":["$","div",null,{"children":"Loading..."}],"children":["$","div",null,{"className":"flex min-h-screen flex-col ","children":["$","$L3",null,{}]}]}],null]]},[null,["$","html",null,{"lang":"en","children":["$","body",null,{"className":"__className_c23dc8","children":["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children"],"loading":"$undefined","loadingStyles":"$undefined","loadingScripts":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[],"styles":null}]}]}],null]],[[["$","link","0",{"rel":"stylesheet","href":"/ui/_next/static/css/a6a9860a7fe022a9.css","precedence":"next","crossOrigin":""}]],"$L6"]]]]
-6:[["$","meta","0",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","meta","1",{"charSet":"utf-8"}],["$","title","2",{"children":"Create Next App"}],["$","meta","3",{"name":"description","content":"Generated by create next app"}],["$","link","4",{"rel":"icon","href":"/ui/favicon.ico","type":"image/x-icon","sizes":"16x16"}],["$","meta","5",{"name":"next-size-adjust"}]]
+0:["p5gDwQBbgW8D3Uz3lgoZg",[[["",{"children":["__PAGE__",{}]},"$undefined","$undefined",true],["",{"children":["__PAGE__",{},["$L1",["$","$2",null,{"fallback":["$","div",null,{"children":"Loading..."}],"children":["$","div",null,{"className":"flex min-h-screen flex-col ","children":["$","$L3",null,{}]}]}],null]]},[null,["$","html",null,{"lang":"en","children":["$","body",null,{"className":"__className_c23dc8","children":["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children"],"loading":"$undefined","loadingStyles":"$undefined","loadingScripts":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[],"styles":null}]}]}],null]],[[["$","link","0",{"rel":"stylesheet","href":"/ui/_next/static/css/a6a9860a7fe022a9.css","precedence":"next","crossOrigin":""}]],"$L6"]]]]
+6:[["$","meta","0",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","meta","1",{"charSet":"utf-8"}],["$","title","2",{"children":"🚅 LiteLLM"}],["$","meta","3",{"name":"description","content":"LiteLLM Proxy Admin UI"}],["$","link","4",{"rel":"icon","href":"/ui/favicon.ico","type":"image/x-icon","sizes":"16x16"}],["$","meta","5",{"name":"next-size-adjust"}]]
1:null
diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py
index d34bdd8c9..3cddecfbe 100644
--- a/litellm/proxy/proxy_server.py
+++ b/litellm/proxy/proxy_server.py
@@ -323,6 +323,7 @@ async def user_api_key_auth(
f"Malformed API Key passed in. Ensure Key has `Bearer ` prefix. Passed in: {passed_in_key}"
)
+ ### CHECK IF ADMIN ###
# note: never string compare api keys, this is vulenerable to a time attack. Use secrets.compare_digest instead
is_master_key_valid = secrets.compare_digest(api_key, master_key)
if is_master_key_valid:
@@ -469,6 +470,12 @@ async def user_api_key_auth(
if _user is None:
continue
assert isinstance(_user, dict)
+ # check if user is admin #
+ if (
+ _user.get("user_role", None) is not None
+ and _user.get("user_role") == "proxy_admin"
+ ):
+ return UserAPIKeyAuth(api_key=master_key)
# Token exists, not expired now check if its in budget for the user
user_max_budget = _user.get("max_budget", None)
user_current_spend = _user.get("spend", None)
@@ -612,10 +619,13 @@ async def user_api_key_auth(
# check if user can access this route
query_params = request.query_params
user_id = query_params.get("user_id")
+ verbose_proxy_logger.debug(
+ f"user_id: {user_id} & valid_token.user_id: {valid_token.user_id}"
+ )
if user_id != valid_token.user_id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="user not allowed to access this key's info",
+ detail="key not allowed to access this user's info",
)
elif route == "/user/update":
raise HTTPException(
@@ -1895,6 +1905,10 @@ async def startup_event():
if prisma_client is not None and master_key is not None:
# add master key to db
+ user_id = "default_user_id"
+ if os.getenv("PROXY_ADMIN_ID", None) is not None:
+ user_id = os.getenv("PROXY_ADMIN_ID")
+
asyncio.create_task(
generate_key_helper_fn(
duration=None,
@@ -1903,7 +1917,12 @@ async def startup_event():
config={},
spend=0,
token=master_key,
- user_id="default_user_id",
+ user_id=user_id,
+ user_role="proxy_admin",
+ query_type="update_data",
+ update_key_values={
+ "user_role": "proxy_admin",
+ },
)
)
@@ -3458,23 +3477,38 @@ async def auth_callback(request: Request):
result = await microsoft_sso.verify_and_process(request)
# User is Authe'd in - generate key for the UI to access Proxy
- user_id = getattr(result, "email", None)
+ user_email = getattr(result, "email", None)
+ user_id = getattr(result, "id", None)
if user_id is None:
user_id = getattr(result, "first_name", "") + getattr(result, "last_name", "")
response = await generate_key_helper_fn(
- **{"duration": "1hr", "key_max_budget": 0, "models": [], "aliases": {}, "config": {}, "spend": 0, "user_id": user_id, "team_id": "litellm-dashboard"} # type: ignore
+ **{"duration": "1hr", "key_max_budget": 0, "models": [], "aliases": {}, "config": {}, "spend": 0, "user_id": user_id, "team_id": "litellm-dashboard", "user_email": user_email} # type: ignore
)
-
key = response["token"] # type: ignore
user_id = response["user_id"] # type: ignore
litellm_dashboard_ui = "/ui/"
+ user_role = "app_owner"
+ if (
+ os.getenv("PROXY_ADMIN_ID", None) is not None
+ and os.environ["PROXY_ADMIN_ID"] == user_id
+ ):
+ # checks if user is admin
+ user_role = "app_admin"
+
import jwt
jwt_token = jwt.encode(
- {"user_id": user_id, "key": key}, "secret", algorithm="HS256"
+ {
+ "user_id": user_id,
+ "key": key,
+ "user_email": user_email,
+ "user_role": user_role,
+ },
+ "secret",
+ algorithm="HS256",
)
litellm_dashboard_ui += "?userID=" + user_id + "&token=" + jwt_token
@@ -3487,10 +3521,18 @@ async def auth_callback(request: Request):
"/user/info", tags=["user management"], dependencies=[Depends(user_api_key_auth)]
)
async def user_info(
- user_id: str = fastapi.Query(..., description="User ID in the request parameters")
+ user_id: Optional[str] = fastapi.Query(
+ default=None, description="User ID in the request parameters"
+ )
):
"""
Use this to get user information. (user row + all user key info)
+
+ Example request
+ ```
+ curl -X GET 'http://localhost:8000/user/info?user_id=krrish7%40berri.ai' \
+ --header 'Authorization: Bearer sk-1234'
+ ```
"""
global prisma_client
try:
@@ -3499,11 +3541,25 @@ async def user_info(
f"Database not connected. Connect a database to your proxy - https://docs.litellm.ai/docs/simple_proxy#managing-auth---virtual-keys"
)
## GET USER ROW ##
- user_info = await prisma_client.get_data(user_id=user_id)
+ if user_id is not None:
+ user_info = await prisma_client.get_data(user_id=user_id)
+ else:
+ user_info = None
## GET ALL KEYS ##
keys = await prisma_client.get_data(
- user_id=user_id, table_name="key", query_type="find_all"
+ user_id=user_id,
+ table_name="key",
+ query_type="find_all",
+ expires=datetime.now(),
)
+
+ if user_info is None:
+ ## make sure we still return a total spend ##
+ spend = 0
+ for k in keys:
+ spend += getattr(k, "spend", 0)
+ user_info = {"spend": spend}
+
## REMOVE HASHED TOKEN INFO before returning ##
for key in keys:
try:
diff --git a/litellm/proxy/utils.py b/litellm/proxy/utils.py
index bbc9a7175..0712c9eb8 100644
--- a/litellm/proxy/utils.py
+++ b/litellm/proxy/utils.py
@@ -565,9 +565,20 @@ class PrismaClient:
# The asterisk before `user_id_list` unpacks the list into separate arguments
response = await self.db.query_raw(sql_query)
elif query_type == "find_all":
- response = await self.db.litellm_usertable.find_many( # type: ignore
- order={"spend": "desc"},
- )
+ if expires is not None:
+ response = await self.db.litellm_usertable.find_many( # type: ignore
+ order={"spend": "desc"},
+ where={ # type:ignore
+ "OR": [
+ {"expires": None}, # type:ignore
+ {"expires": {"gt": expires}}, # type:ignore
+ ],
+ },
+ )
+ else:
+ response = await self.db.litellm_usertable.find_many( # type: ignore
+ order={"spend": "desc"},
+ )
return response
elif table_name == "spend":
verbose_proxy_logger.debug(
diff --git a/litellm/tests/test_key_generate_dynamodb.py b/litellm/tests/test_key_generate_dynamodb.py
index e77dc7472..573bd944d 100644
--- a/litellm/tests/test_key_generate_dynamodb.py
+++ b/litellm/tests/test_key_generate_dynamodb.py
@@ -513,4 +513,4 @@ def test_dynamo_db_migration(custom_db_client):
asyncio.run(test())
except Exception as e:
- pytest.fail(f"An exception occurred - {str(e)}")
+ pytest.fail(f"An exception occurred - {traceback.format_exc()}")
diff --git a/ui/litellm-dashboard/out/404.html b/ui/litellm-dashboard/out/404.html
index 1ec6cd9a4..c57eb5193 100644
--- a/ui/litellm-dashboard/out/404.html
+++ b/ui/litellm-dashboard/out/404.html
@@ -1 +1 @@
-404: This page could not be found.Create Next App404
This page could not be found.
\ No newline at end of file
+404: This page could not be found.🚅 LiteLLM404
This page could not be found.
\ No newline at end of file
diff --git a/ui/litellm-dashboard/out/_next/static/chunks/app/layout-4d667c133e03c98b.js b/ui/litellm-dashboard/out/_next/static/chunks/app/layout-4d667c133e03c98b.js
deleted file mode 100644
index e261adc05..000000000
--- a/ui/litellm-dashboard/out/_next/static/chunks/app/layout-4d667c133e03c98b.js
+++ /dev/null
@@ -1 +0,0 @@
-(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[185],{87421:function(n,e,t){Promise.resolve().then(t.t.bind(t,99646,23)),Promise.resolve().then(t.t.bind(t,63385,23))},63385:function(){},99646:function(n){n.exports={style:{fontFamily:"'__Inter_c23dc8', '__Inter_Fallback_c23dc8'",fontStyle:"normal"},className:"__className_c23dc8"}}},function(n){n.O(0,[971,69,744],function(){return n(n.s=87421)}),_N_E=n.O()}]);
\ No newline at end of file
diff --git a/ui/litellm-dashboard/out/_next/static/chunks/app/page-992f4cdd1053ee86.js b/ui/litellm-dashboard/out/_next/static/chunks/app/page-992f4cdd1053ee86.js
deleted file mode 100644
index cd4ccb43c..000000000
--- a/ui/litellm-dashboard/out/_next/static/chunks/app/page-992f4cdd1053ee86.js
+++ /dev/null
@@ -1 +0,0 @@
-(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[931],{88102:function(e,t,l){Promise.resolve().then(l.bind(l,27680))},27680:function(e,t,l){"use strict";l.r(t),l.d(t,{default:function(){return M}});var r=l(3827),s=l(64090),n=l(80588);let a=async(e,t,l)=>{try{if(console.log("Form Values in keyCreateCall:",l),l.description&&(l.metadata||(l.metadata={}),l.metadata.description=l.description,delete l.description,l.metadata=JSON.stringify(l.metadata)),l.metadata){console.log("formValues.metadata:",l.metadata);try{l.metadata=JSON.parse(l.metadata)}catch(e){throw n.ZP.error("Failed to parse metadata: "+e),Error("Failed to parse metadata: "+e)}}console.log("Form Values after check:",l);let r=await fetch("/key/generate",{method:"POST",headers:{Authorization:"Bearer ".concat(e),"Content-Type":"application/json"},body:JSON.stringify({user_id:t,...l})});if(!r.ok){let e=await r.text();throw n.ZP.error("Failed to create key: "+e),console.error("Error response from the server:",e),Error("Network response was not ok")}let s=await r.json();return console.log("API Response:",s),s}catch(e){throw console.error("Failed to create key:",e),e}},o=async(e,t)=>{try{console.log("in keyDeleteCall:",t);let l=await fetch("/key/delete",{method:"POST",headers:{Authorization:"Bearer ".concat(e),"Content-Type":"application/json"},body:JSON.stringify({keys:[t]})});if(!l.ok){let e=await l.text();throw n.ZP.error("Failed to delete key: "+e),Error("Network response was not ok")}let r=await l.json();return console.log(r),n.ZP.success("API Key Deleted"),r}catch(e){throw console.error("Failed to create key:",e),e}},i=async(e,t)=>{try{let l="/user/info";console.log("in userInfoCall:",l);let r=await fetch("".concat(l,"/?user_id=").concat(t),{method:"GET",headers:{Authorization:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!r.ok){let e=await r.text();throw n.ZP.error(e),Error("Network response was not ok")}let s=await r.json();return console.log(s),s}catch(e){throw console.error("Failed to create key:",e),e}},c=async(e,t)=>{try{let l="/spend/logs";console.log("in keySpendLogsCall:",l);let r=await fetch("".concat(l,"/?api_key=").concat(t),{method:"GET",headers:{Authorization:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!r.ok){let e=await r.text();throw n.ZP.error(e),Error("Network response was not ok")}let s=await r.json();return console.log(s),s}catch(e){throw console.error("Failed to create key:",e),e}};var d=l(10384),h=l(46453),u=l(2179),m=l(71801),x=l(96776),j=l(2902),p=l(77171),y=l(29714),Z=l(88707),g=l(1861);let{Option:f}=x.default;var w=e=>{let{userID:t,userRole:l,accessToken:o,data:i,setData:c}=e,[x]=j.Z.useForm(),[f,w]=(0,s.useState)(!1),[k,b]=(0,s.useState)(null),N=()=>{w(!1),x.resetFields()},S=()=>{w(!1),b(null),x.resetFields()},_=async e=>{try{n.ZP.info("Making API Call"),e.models&&""!==e.models.trim()?e.models=e.models.split(",").map(e=>e.trim()):e.models=[],w(!0);let l=await a(o,t,e);c(e=>e?[...e,l]:[l]),b(l.key),n.ZP.success("API Key Created"),x.resetFields()}catch(e){console.error("Error creating the key:",e)}};return(0,r.jsxs)("div",{children:[(0,r.jsx)(u.Z,{className:"mx-auto",onClick:()=>w(!0),children:"+ Create New Key"}),(0,r.jsx)(p.Z,{title:"Create Key",visible:f,width:800,footer:null,onOk:N,onCancel:S,children:(0,r.jsxs)(j.Z,{form:x,onFinish:_,labelCol:{span:6},wrapperCol:{span:16},labelAlign:"left",children:["App Owner"===l||"Admin"===l?(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(j.Z.Item,{label:"Key Name",name:"key_alias",children:(0,r.jsx)(y.Z,{})}),(0,r.jsx)(j.Z.Item,{label:"Team ID",name:"team_id",children:(0,r.jsx)(y.Z,{placeholder:"ai_team"})}),(0,r.jsx)(j.Z.Item,{label:"Models (Comma Separated). Eg: gpt-3.5-turbo,gpt-4",name:"models",children:(0,r.jsx)(y.Z,{placeholder:"gpt-4,gpt-3.5-turbo"})}),(0,r.jsx)(j.Z.Item,{label:"Max Budget (USD)",name:"max_budget",children:(0,r.jsx)(Z.Z,{step:.01,precision:2,width:200})}),(0,r.jsx)(j.Z.Item,{label:"Duration (eg: 30s, 30h, 30d)",name:"duration",children:(0,r.jsx)(y.Z,{})}),(0,r.jsx)(j.Z.Item,{label:"Metadata",name:"metadata",children:(0,r.jsx)(y.Z.TextArea,{rows:4,placeholder:"Enter metadata as JSON"})})]}):(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(j.Z.Item,{label:"Key Name",name:"key_alias",children:(0,r.jsx)(y.Z,{})}),(0,r.jsx)(j.Z.Item,{label:"Team ID (Contact Group)",name:"team_id",children:(0,r.jsx)(y.Z,{placeholder:"ai_team"})}),(0,r.jsx)(j.Z.Item,{label:"Description",name:"description",children:(0,r.jsx)(y.Z.TextArea,{placeholder:"Enter description",rows:4})})]}),(0,r.jsx)("div",{style:{textAlign:"right",marginTop:"10px"},children:(0,r.jsx)(g.ZP,{htmlType:"submit",children:"Create Key"})})]})}),k&&(0,r.jsx)(p.Z,{title:"Save your key",visible:f,onOk:N,onCancel:S,footer:null,children:(0,r.jsxs)(h.Z,{numItems:1,className:"gap-2 w-full",children:[(0,r.jsx)(d.Z,{numColSpan:1,children:(0,r.jsxs)("p",{children:["Please save this secret key somewhere safe and accessible. For security reasons, ",(0,r.jsx)("b",{children:"you will not be able to view it again"})," ","through your LiteLLM account. If you lose this secret key, you will need to generate a new one."]})}),(0,r.jsx)(d.Z,{numColSpan:1,children:null!=k?(0,r.jsxs)(m.Z,{children:["API Key: ",k]}):(0,r.jsx)(m.Z,{children:"Key being created, this might take 30s"})})]})})]})},k=l(33393),b=l(13810),N=l(61244),S=l(10827),_=l(3851),D=l(2044),C=l(64167),v=l(74480),I=l(7178),E=l(42440),T=l(9853),A=l(67989),O=l(56863),F=e=>{let{token:t,accessToken:l,keySpend:n,keyBudget:a,keyName:o}=e,[i,d]=(0,s.useState)(!1),[h,m]=(0,s.useState)(null),[x,j]=(0,s.useState)(null),y=async()=>{try{if(null==l||null==t)return;let e=await c(l,t);console.log("Response:",e);let r=Object.values(e).reduce((e,t)=>{let l=new Date(t.startTime),r=new Intl.DateTimeFormat("en-US",{day:"2-digit",month:"short"}).format(l);return e[r]=(e[r]||0)+t.spend,e},{}),s=Object.entries(r);s.sort((e,t)=>{let[l]=e,[r]=t,s=new Date(l),n=new Date(r);return s.getTime()-n.getTime()});let n=Object.fromEntries(s);console.log(n);let a=Object.values(e).reduce((e,t)=>{let l=t.user;return e[l]=(e[l]||0)+t.spend,e},{});console.log(r),console.log(a);let o=[];for(let[e,t]of Object.entries(n))o.push({day:e,spend:t});let i=Object.entries(a).sort((e,t)=>t[1]-e[1]).slice(0,5).map(e=>{let[t,l]=e;return{name:t,value:l}});m(o),j(i),console.log("arrayBarChart:",o)}catch(e){console.error("There was an error fetching the data",e)}};return((0,s.useEffect)(()=>{y()},[t]),t)?(0,r.jsxs)("div",{children:[(0,r.jsx)(u.Z,{className:"mx-auto",onClick:()=>{d(!0)},children:"View Spend Report"}),(0,r.jsxs)(p.Z,{visible:i,width:1e3,onOk:()=>{d(!1)},onCancel:()=>{d(!1)},footer:null,children:[(0,r.jsxs)(E.Z,{style:{textAlign:"left"},children:["Key Name: ",o]}),(0,r.jsxs)(O.Z,{children:["Monthly Spend $",n]}),(0,r.jsx)(b.Z,{className:"mt-6 mb-6",children:h&&(0,r.jsx)(T.Z,{className:"mt-6",data:h,colors:["green"],index:"day",categories:["spend"],yAxisWidth:48})}),(0,r.jsx)(E.Z,{className:"mt-6",children:"Top 5 Users Spend (USD)"}),(0,r.jsx)(b.Z,{className:"mb-6",children:x&&(0,r.jsx)(A.Z,{className:"mt-6",data:x,color:"teal"})})]})]}):null},P=e=>{let{userID:t,accessToken:l,data:s,setData:n}=e,a=async e=>{if(null!=s)try{await o(l,e);let t=s.filter(t=>t.token!==e);n(t)}catch(e){console.error("Error deleting the key:",e)}};if(null!=s)return console.log("RERENDER TRIGGERED"),(0,r.jsxs)(b.Z,{className:"w-full mx-auto flex-auto overflow-y-auto max-h-[50vh] mb-4",children:[(0,r.jsx)(E.Z,{children:"API Keys"}),(0,r.jsxs)(S.Z,{className:"mt-5",children:[(0,r.jsx)(C.Z,{children:(0,r.jsxs)(I.Z,{children:[(0,r.jsx)(v.Z,{children:"Key Alias"}),(0,r.jsx)(v.Z,{children:"Secret Key"}),(0,r.jsx)(v.Z,{children:"Spend (USD)"}),(0,r.jsx)(v.Z,{children:"Key Budget (USD)"}),(0,r.jsx)(v.Z,{children:"Team ID"}),(0,r.jsx)(v.Z,{children:"Metadata"}),(0,r.jsx)(v.Z,{children:"Expires"})]})}),(0,r.jsx)(_.Z,{children:s.map(e=>(console.log(e),"litellm-dashboard"===e.team_id)?null:(0,r.jsxs)(I.Z,{children:[(0,r.jsx)(D.Z,{children:null!=e.key_alias?(0,r.jsx)(m.Z,{children:e.key_alias}):(0,r.jsx)(m.Z,{children:"Not Set"})}),(0,r.jsx)(D.Z,{children:(0,r.jsx)(m.Z,{children:e.key_name})}),(0,r.jsx)(D.Z,{children:(0,r.jsx)(m.Z,{children:e.spend})}),(0,r.jsx)(D.Z,{children:null!=e.max_budget?(0,r.jsx)(m.Z,{children:e.max_budget}):(0,r.jsx)(m.Z,{children:"Unlimited Budget"})}),(0,r.jsx)(D.Z,{children:(0,r.jsx)(m.Z,{children:e.team_id})}),(0,r.jsx)(D.Z,{children:(0,r.jsx)(m.Z,{children:JSON.stringify(e.metadata)})}),(0,r.jsx)(D.Z,{children:null!=e.expires?(0,r.jsx)(m.Z,{children:e.expires}):(0,r.jsx)(m.Z,{children:"Never expires"})}),(0,r.jsx)(D.Z,{children:(0,r.jsx)(N.Z,{onClick:()=>a(e.token),icon:k.Z,size:"sm"})}),(0,r.jsx)(D.Z,{children:(0,r.jsx)(F,{token:e.token,accessToken:l,keySpend:e.spend,keyBudget:e.max_budget,keyName:e.key_name})})]},e.token))})]})]})},R=e=>{let{userID:t,userSpendData:l}=e;console.log("User SpendData:",l);let s=null==l?void 0:l.spend,n=(null==l?void 0:l.max_budget)||null,a=null!==n?"$".concat(n," limit"):"No limit";return"$".concat(s," / ").concat(a),(0,r.jsx)(r.Fragment,{children:(0,r.jsxs)(b.Z,{className:"mx-auto mb-4",children:[(0,r.jsxs)(O.Z,{children:["$",s]}),(0,r.jsxs)(E.Z,{children:["/ ",a]})]})})},K=l(8792),U=e=>{let{userID:t,userRole:l}=e;return console.log("User ID:",t),(0,r.jsxs)("nav",{className:"left-0 right-0 top-0 flex justify-between items-center h-12 mb-4",children:[(0,r.jsx)("div",{className:"text-left mx-4 my-2 absolute top-0 left-0",children:(0,r.jsx)("div",{className:"flex flex-col items-center",children:(0,r.jsx)(K.default,{href:"/",children:(0,r.jsx)("button",{className:"text-gray-800 text-2xl px-4 py-1 rounded text-center",children:"\uD83D\uDE85 LiteLLM"})})})}),(0,r.jsx)("div",{className:"text-right mx-4 my-2 absolute top-0 right-0",children:(0,r.jsxs)(u.Z,{variant:"secondary",children:[t,(0,r.jsxs)("p",{children:["Role: ",l]})]})})]})},B=l(47907),L=l(37963),M=()=>{let[e,t]=(0,s.useState)(null),[l,n]=(0,s.useState)(null),a=(0,B.useSearchParams)(),o=a.get("userID");a.get("viewSpend");let c=a.get("token"),[u,m]=(0,s.useState)(null),[x,j]=(0,s.useState)(null);if((0,s.useEffect)(()=>{if(c){let e=(0,L.o)(c);if(e){if(console.log("Decoded token:",e),console.log("Decoded key:",e.key),m(e.key),e.user_role){let t=function(e){if(!e)return"Undefined Role";switch(e.toLowerCase()){case"app_owner":return"App Owner";case"demo_app_owner":return"AppOwner";case"admin":return"Admin";case"app_user":return"App User";default:return"Unknown Role"}}(e.user_role);console.log("Decoded user_role:",t),j(t)}else console.log("User role not defined")}}o&&u&&!e&&(async()=>{try{let e=await i(u,o);console.log("Response:",e),n(e.user_info),t(e.keys)}catch(e){console.error("There was an error fetching the data",e)}})()},[o,c,u,e]),null==o||null==c){let e="/sso/key/generate";return console.log("Full URL:",e),window.location.href=e,null}return null==u?null:(null==x&&j("App Owner"),(0,r.jsxs)("div",{children:[(0,r.jsx)(U,{userID:o,userRole:x}),(0,r.jsx)(h.Z,{numItems:1,className:"gap-0 p-10 h-[75vh] w-full",children:(0,r.jsxs)(d.Z,{numColSpan:1,children:[(0,r.jsx)(R,{userID:o,userSpendData:l}),(0,r.jsx)(P,{userID:o,accessToken:u,data:e,setData:t}),(0,r.jsx)(w,{userID:o,userRole:x,accessToken:u,data:e,setData:t})]})})]}))}}},function(e){e.O(0,[787,971,69,744],function(){return e(e.s=88102)}),_N_E=e.O()}]);
\ No newline at end of file
diff --git a/ui/litellm-dashboard/out/_next/static/lGjwnJSGwBqa476jHHI8W/_buildManifest.js b/ui/litellm-dashboard/out/_next/static/lGjwnJSGwBqa476jHHI8W/_buildManifest.js
deleted file mode 100644
index f779caa02..000000000
--- a/ui/litellm-dashboard/out/_next/static/lGjwnJSGwBqa476jHHI8W/_buildManifest.js
+++ /dev/null
@@ -1 +0,0 @@
-self.__BUILD_MANIFEST={__rewrites:{afterFiles:[],beforeFiles:[],fallback:[]},"/_error":["static/chunks/pages/_error-d6107f1aac0c574c.js"],sortedPages:["/_app","/_error"]},self.__BUILD_MANIFEST_CB&&self.__BUILD_MANIFEST_CB();
\ No newline at end of file
diff --git a/ui/litellm-dashboard/out/_next/static/lGjwnJSGwBqa476jHHI8W/_ssgManifest.js b/ui/litellm-dashboard/out/_next/static/lGjwnJSGwBqa476jHHI8W/_ssgManifest.js
deleted file mode 100644
index 5b3ff592f..000000000
--- a/ui/litellm-dashboard/out/_next/static/lGjwnJSGwBqa476jHHI8W/_ssgManifest.js
+++ /dev/null
@@ -1 +0,0 @@
-self.__SSG_MANIFEST=new Set([]);self.__SSG_MANIFEST_CB&&self.__SSG_MANIFEST_CB()
\ No newline at end of file
diff --git a/ui/litellm-dashboard/out/index.html b/ui/litellm-dashboard/out/index.html
index f0fb6f14c..9537fb723 100644
--- a/ui/litellm-dashboard/out/index.html
+++ b/ui/litellm-dashboard/out/index.html
@@ -1 +1 @@
-Create Next AppLoading...
\ No newline at end of file
+🚅 LiteLLMLoading...
\ No newline at end of file
diff --git a/ui/litellm-dashboard/out/index.txt b/ui/litellm-dashboard/out/index.txt
index db2a9c631..f48954f2c 100644
--- a/ui/litellm-dashboard/out/index.txt
+++ b/ui/litellm-dashboard/out/index.txt
@@ -1,7 +1,7 @@
2:"$Sreact.suspense"
-3:I[27680,["787","static/chunks/787-5bb33960644f5c7c.js","931","static/chunks/app/page-992f4cdd1053ee86.js"],""]
+3:I[27680,["787","static/chunks/787-5bb33960644f5c7c.js","931","static/chunks/app/page-7f03ccc8529ada97.js"],""]
4:I[5613,[],""]
5:I[31778,[],""]
-0:["lGjwnJSGwBqa476jHHI8W",[[["",{"children":["__PAGE__",{}]},"$undefined","$undefined",true],["",{"children":["__PAGE__",{},["$L1",["$","$2",null,{"fallback":["$","div",null,{"children":"Loading..."}],"children":["$","div",null,{"className":"flex min-h-screen flex-col ","children":["$","$L3",null,{}]}]}],null]]},[null,["$","html",null,{"lang":"en","children":["$","body",null,{"className":"__className_c23dc8","children":["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children"],"loading":"$undefined","loadingStyles":"$undefined","loadingScripts":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[],"styles":null}]}]}],null]],[[["$","link","0",{"rel":"stylesheet","href":"/ui/_next/static/css/a6a9860a7fe022a9.css","precedence":"next","crossOrigin":""}]],"$L6"]]]]
-6:[["$","meta","0",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","meta","1",{"charSet":"utf-8"}],["$","title","2",{"children":"Create Next App"}],["$","meta","3",{"name":"description","content":"Generated by create next app"}],["$","link","4",{"rel":"icon","href":"/ui/favicon.ico","type":"image/x-icon","sizes":"16x16"}],["$","meta","5",{"name":"next-size-adjust"}]]
+0:["p5gDwQBbgW8D3Uz3lgoZg",[[["",{"children":["__PAGE__",{}]},"$undefined","$undefined",true],["",{"children":["__PAGE__",{},["$L1",["$","$2",null,{"fallback":["$","div",null,{"children":"Loading..."}],"children":["$","div",null,{"className":"flex min-h-screen flex-col ","children":["$","$L3",null,{}]}]}],null]]},[null,["$","html",null,{"lang":"en","children":["$","body",null,{"className":"__className_c23dc8","children":["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children"],"loading":"$undefined","loadingStyles":"$undefined","loadingScripts":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[],"styles":null}]}]}],null]],[[["$","link","0",{"rel":"stylesheet","href":"/ui/_next/static/css/a6a9860a7fe022a9.css","precedence":"next","crossOrigin":""}]],"$L6"]]]]
+6:[["$","meta","0",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","meta","1",{"charSet":"utf-8"}],["$","title","2",{"children":"🚅 LiteLLM"}],["$","meta","3",{"name":"description","content":"LiteLLM Proxy Admin UI"}],["$","link","4",{"rel":"icon","href":"/ui/favicon.ico","type":"image/x-icon","sizes":"16x16"}],["$","meta","5",{"name":"next-size-adjust"}]]
1:null
diff --git a/ui/litellm-dashboard/src/components/navbar.tsx b/ui/litellm-dashboard/src/components/navbar.tsx
index b7cb35730..946cfc447 100644
--- a/ui/litellm-dashboard/src/components/navbar.tsx
+++ b/ui/litellm-dashboard/src/components/navbar.tsx
@@ -1,40 +1,50 @@
"use client";
-import Link from 'next/link';
-import Image from 'next/image'
-import React, { useState } from 'react';
+import Link from "next/link";
+import Image from "next/image";
+import React, { useState } from "react";
import { useSearchParams } from "next/navigation";
-import { Button, Text, Metric,Title, TextInput, Grid, Col, Card } from "@tremor/react";
+import {
+ Button,
+ Text,
+ Metric,
+ Title,
+ TextInput,
+ Grid,
+ Col,
+ Card,
+} from "@tremor/react";
// Define the props type
interface NavbarProps {
- userID: string | null;
- userRole: string | null;
+ userID: string | null;
+ userRole: string | null;
+ userEmail: string | null;
}
-const Navbar: React.FC = ({ userID, userRole }) => {
- console.log("User ID:", userID);
+const Navbar: React.FC = ({ userID, userRole, userEmail }) => {
+ console.log("User ID:", userID);
+ console.log("userEmail:", userEmail);
- return (
-
- )
-}
+ return (
+
+ );
+};
-export default Navbar;
\ No newline at end of file
+export default Navbar;
diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx
index 4763e475e..12eea2dd8 100644
--- a/ui/litellm-dashboard/src/components/networking.tsx
+++ b/ui/litellm-dashboard/src/components/networking.tsx
@@ -1,24 +1,24 @@
/**
* Helper file for calls being made to proxy
*/
-import { message } from 'antd';
+import { message } from "antd";
-const proxyBaseUrl = null;
-// const proxyBaseUrl = "http://localhost:4000" // http://localhost:4000
+const isLocal = process.env.NODE_ENV === "development";
+const proxyBaseUrl = isLocal ? "http://localhost:4000" : null;
export const keyCreateCall = async (
accessToken: string,
userID: string,
- formValues: Record, // Assuming formValues is an object
+ formValues: Record // Assuming formValues is an object
) => {
try {
- console.log("Form Values in keyCreateCall:", formValues); // Log the form values before making the API call
-
+ console.log("Form Values in keyCreateCall:", formValues); // Log the form values before making the API call
+
// check if formValues.description is not undefined, make it a string and add it to formValues.metadata
if (formValues.description) {
// add to formValues.metadata
if (!formValues.metadata) {
- formValues.metadata = {}
+ formValues.metadata = {};
}
// value needs to be in "", valid JSON
formValues.metadata.description = formValues.description;
@@ -26,7 +26,7 @@ export const keyCreateCall = async (
delete formValues.description;
formValues.metadata = JSON.stringify(formValues.metadata);
}
- // if formValues.metadata is not undefined, make it a valid dict
+ // if formValues.metadata is not undefined, make it a valid dict
if (formValues.metadata) {
console.log("formValues.metadata:", formValues.metadata);
// if there's an exception JSON.parse, show it in the message
@@ -69,15 +69,11 @@ export const keyCreateCall = async (
}
};
-
-export const keyDeleteCall = async (
- accessToken: String,
- user_key: String
-) => {
+export const keyDeleteCall = async (accessToken: String, user_key: String) => {
try {
const url = proxyBaseUrl ? `${proxyBaseUrl}/key/delete` : `/key/delete`;
- console.log("in keyDeleteCall:", user_key)
-
+ console.log("in keyDeleteCall:", user_key);
+ message.info("Making key delete request");
const response = await fetch(url, {
method: "POST",
headers: {
@@ -108,21 +104,22 @@ export const keyDeleteCall = async (
export const userInfoCall = async (
accessToken: String,
- userID: String
+ userID: String,
+ userRole: String
) => {
try {
- const url = proxyBaseUrl ? `${proxyBaseUrl}/user/info` : `/user/info`;
- console.log("in userInfoCall:", url)
- const response = await fetch(
- `${url}/?user_id=${userID}`,
- {
- method: "GET",
- headers: {
- Authorization: `Bearer ${accessToken}`,
- "Content-Type": "application/json",
- },
- }
- );
+ let url = proxyBaseUrl ? `${proxyBaseUrl}/user/info` : `/user/info`;
+ if (userRole == "App Owner") {
+ url = `${url}/?user_id=${userID}`;
+ }
+ message.info("Requesting user data");
+ const response = await fetch(url, {
+ method: "GET",
+ headers: {
+ Authorization: `Bearer ${accessToken}`,
+ "Content-Type": "application/json",
+ },
+ });
if (!response.ok) {
const errorData = await response.text();
@@ -131,7 +128,7 @@ export const userInfoCall = async (
}
const data = await response.json();
- console.log(data);
+ message.info("Received user data");
return data;
// Handle success - you might want to update some state or UI based on the created key
} catch (error) {
@@ -140,24 +137,17 @@ export const userInfoCall = async (
}
};
-
-export const keySpendLogsCall = async (
- accessToken: String,
- token: String
-) => {
+export const keySpendLogsCall = async (accessToken: String, token: String) => {
try {
const url = proxyBaseUrl ? `${proxyBaseUrl}/spend/logs` : `/spend/logs`;
- console.log("in keySpendLogsCall:", url)
- const response = await fetch(
- `${url}/?api_key=${token}`,
- {
- method: "GET",
- headers: {
- Authorization: `Bearer ${accessToken}`,
- "Content-Type": "application/json",
- },
- }
- );
+ console.log("in keySpendLogsCall:", url);
+ const response = await fetch(`${url}/?api_key=${token}`, {
+ method: "GET",
+ headers: {
+ Authorization: `Bearer ${accessToken}`,
+ "Content-Type": "application/json",
+ },
+ });
if (!response.ok) {
const errorData = await response.text();
message.error(errorData);
@@ -171,4 +161,4 @@ export const keySpendLogsCall = async (
console.error("Failed to create key:", error);
throw error;
}
-}
+};
diff --git a/ui/litellm-dashboard/src/components/user_dashboard.tsx b/ui/litellm-dashboard/src/components/user_dashboard.tsx
index 951d0287b..b1a06939b 100644
--- a/ui/litellm-dashboard/src/components/user_dashboard.tsx
+++ b/ui/litellm-dashboard/src/components/user_dashboard.tsx
@@ -6,21 +6,25 @@ import CreateKey from "./create_key_button";
import ViewKeyTable from "./view_key_table";
import ViewUserSpend from "./view_user_spend";
import EnterProxyUrl from "./enter_proxy_url";
+import { message } from "antd";
import Navbar from "./navbar";
import { useSearchParams } from "next/navigation";
import { jwtDecode } from "jwt-decode";
-const proxyBaseUrl = null;
-// const proxyBaseUrl = "http://localhost:4000" // http://localhost:4000
+const isLocal = process.env.NODE_ENV === "development";
+console.log("isLocal:", isLocal);
+const proxyBaseUrl = isLocal ? "http://localhost:4000" : null;
type UserSpendData = {
spend: number;
max_budget?: number | null;
-}
+};
const UserDashboard = () => {
const [data, setData] = useState(null); // Keep the initialization of state here
- const [userSpendData, setUserSpendData] = useState(null);
+ const [userSpendData, setUserSpendData] = useState(
+ null
+ );
// Assuming useSearchParams() hook exists and works in your setup
const searchParams = useSearchParams();
@@ -30,19 +34,19 @@ const UserDashboard = () => {
const token = searchParams.get("token");
const [accessToken, setAccessToken] = useState(null);
const [userRole, setUserRole] = useState(null);
-
+ const [userEmail, setUserEmail] = useState(null);
function formatUserRole(userRole: string) {
if (!userRole) {
return "Undefined Role";
}
-
+ console.log(`Received user role: ${userRole}`);
switch (userRole.toLowerCase()) {
case "app_owner":
return "App Owner";
case "demo_app_owner":
- return "AppOwner";
- case "admin":
+ return "App Owner";
+ case "app_admin":
return "Admin";
case "app_user":
return "App User";
@@ -53,7 +57,7 @@ const UserDashboard = () => {
// Moved useEffect inside the component and used a condition to run fetch only if the params are available
useEffect(() => {
- if (token){
+ if (token) {
const decoded = jwtDecode(token) as { [key: string]: any };
if (decoded) {
// cast decoded to dictionary
@@ -71,17 +75,19 @@ const UserDashboard = () => {
} else {
console.log("User role not defined");
}
+
+ if (decoded.user_email) {
+ setUserEmail(decoded.user_email);
+ } else {
+ console.log(`User Email is not set ${decoded}`);
+ }
}
}
- if (userID && accessToken && !data) {
+ if (userID && accessToken && userRole && !data) {
const fetchData = async () => {
try {
- const response = await userInfoCall(
- accessToken,
- userID
- );
- console.log("Response:", response);
- setUserSpendData(response["user_info"])
+ const response = await userInfoCall(accessToken, userID, userRole);
+ setUserSpendData(response["user_info"]);
setData(response["keys"]); // Assuming this is the correct path to your data
} catch (error) {
console.error("There was an error fetching the data", error);
@@ -93,53 +99,45 @@ const UserDashboard = () => {
}, [userID, token, accessToken, data]);
if (userID == null || token == null) {
-
-
// Now you can construct the full URL
- const url = proxyBaseUrl ? `${proxyBaseUrl}/sso/key/generate` : `/sso/key/generate`;
+ const url = proxyBaseUrl
+ ? `${proxyBaseUrl}/sso/key/generate`
+ : `/sso/key/generate`;
console.log("Full URL:", url);
window.location.href = url;
return null;
- }
- else if (accessToken == null) {
+ } else if (accessToken == null) {
return null;
}
if (userRole == null) {
- setUserRole("App Owner")
+ setUserRole("App Owner");
}
-
+
return (
-
+
-
-
-
-
-
-
+
+
+
+
+
+
-
);
};
-export default UserDashboard;
\ No newline at end of file
+export default UserDashboard;
diff --git a/ui/litellm-dashboard/src/components/view_key_spend_report.tsx b/ui/litellm-dashboard/src/components/view_key_spend_report.tsx
index 40961325e..e90401e5b 100644
--- a/ui/litellm-dashboard/src/components/view_key_spend_report.tsx
+++ b/ui/litellm-dashboard/src/components/view_key_spend_report.tsx
@@ -1,8 +1,26 @@
"use client";
import React, { useState, useEffect } from "react";
-import { Button as Button2, Modal, Form, Input, InputNumber, Select, message } from "antd";
-import { Button, Text, Card, Table, BarChart, Title, Subtitle, BarList, Metric } from "@tremor/react";
+import {
+ Button as Button2,
+ Modal,
+ Form,
+ Input,
+ InputNumber,
+ Select,
+ message,
+} from "antd";
+import {
+ Button,
+ Text,
+ Card,
+ Table,
+ BarChart,
+ Title,
+ Subtitle,
+ BarList,
+ Metric,
+} from "@tremor/react";
import { keySpendLogsCall } from "./networking";
interface ViewKeySpendReportProps {
@@ -14,18 +32,30 @@ interface ViewKeySpendReportProps {
}
type ResponseValueType = {
- startTime: string; // Assuming startTime is a string, adjust it if it's of a different type
- spend: number; // Assuming spend is a number, adjust it if it's of a different type
- user: string; // Assuming user is a string, adjust it if it's of a different type
- };
+ startTime: string; // Assuming startTime is a string, adjust it if it's of a different type
+ spend: number; // Assuming spend is a number, adjust it if it's of a different type
+ user: string; // Assuming user is a string, adjust it if it's of a different type
+};
-const ViewKeySpendReport: React.FC = ({ token, accessToken, keySpend, keyBudget, keyName }) => {
+const ViewKeySpendReport: React.FC = ({
+ token,
+ accessToken,
+ keySpend,
+ keyBudget,
+ keyName,
+}) => {
const [isModalVisible, setIsModalVisible] = useState(false);
- const [data, setData] = useState<{ day: string; spend: number; }[] | null>(null);
- const [userData, setUserData] = useState<{ name: string; value: number; }[] | null>(null);
+ const [data, setData] = useState<{ day: string; spend: number }[] | null>(
+ null
+ );
+ const [userData, setUserData] = useState<
+ { name: string; value: number }[] | null
+ >(null);
const showModal = () => {
+ console.log("Show Modal triggered");
setIsModalVisible(true);
+ fetchData();
};
const handleOk = () => {
@@ -41,68 +71,79 @@ const ViewKeySpendReport: React.FC = ({ token, accessTo
try {
if (accessToken == null || token == null) {
return;
- }
- const response = await keySpendLogsCall(accessToken=accessToken, token=token);
+ }
+ console.log(`accessToken: ${accessToken}; token: ${token}`);
+ const response = await keySpendLogsCall(
+ (accessToken = accessToken),
+ (token = token)
+ );
console.log("Response:", response);
// loop through response
// get spend, startTime for each element, place in new array
-
- const pricePerDay: Record = (Object.values(response) as ResponseValueType[]).reduce((acc: Record, value) => {
+ const pricePerDay: Record = (
+ Object.values(response) as ResponseValueType[]
+ ).reduce((acc: Record, value) => {
const startTime = new Date(value.startTime);
- const day = new Intl.DateTimeFormat('en-US', { day: '2-digit', month: 'short' }).format(startTime);
-
+ const day = new Intl.DateTimeFormat("en-US", {
+ day: "2-digit",
+ month: "short",
+ }).format(startTime);
+
acc[day] = (acc[day] || 0) + value.spend;
-
+
return acc;
}, {});
-
-
+
// sort pricePerDay by day
// Convert object to array of key-value pairs
- const pricePerDayArray = Object.entries(pricePerDay);
+ const pricePerDayArray = Object.entries(pricePerDay);
- // Sort the array based on the date (key)
- pricePerDayArray.sort(([aKey], [bKey]) => {
- const dateA = new Date(aKey);
- const dateB = new Date(bKey);
- return dateA.getTime() - dateB.getTime();
- });
-
- // Convert the sorted array back to an object
- const sortedPricePerDay = Object.fromEntries(pricePerDayArray);
+ // Sort the array based on the date (key)
+ pricePerDayArray.sort(([aKey], [bKey]) => {
+ const dateA = new Date(aKey);
+ const dateB = new Date(bKey);
+ return dateA.getTime() - dateB.getTime();
+ });
+ // Convert the sorted array back to an object
+ const sortedPricePerDay = Object.fromEntries(pricePerDayArray);
console.log(sortedPricePerDay);
-
- const pricePerUser: Record = (Object.values(response) as ResponseValueType[]).reduce((acc: Record, value) => {
+
+ const pricePerUser: Record = (
+ Object.values(response) as ResponseValueType[]
+ ).reduce((acc: Record, value) => {
const user = value.user;
acc[user] = (acc[user] || 0) + value.spend;
-
+
return acc;
}, {});
-
-
+
console.log(pricePerDay);
console.log(pricePerUser);
const arrayBarChart = [];
- // [
- // {
- // "day": "02 Feb",
- // "spend": pricePerDay["02 Feb"],
- // }
- // ]
+ // [
+ // {
+ // "day": "02 Feb",
+ // "spend": pricePerDay["02 Feb"],
+ // }
+ // ]
for (const [key, value] of Object.entries(sortedPricePerDay)) {
arrayBarChart.push({ day: key, spend: value });
}
-
// get 5 most expensive users
- const sortedUsers = Object.entries(pricePerUser).sort((a, b) => b[1] - a[1]);
+ const sortedUsers = Object.entries(pricePerUser).sort(
+ (a, b) => b[1] - a[1]
+ );
const top5Users = sortedUsers.slice(0, 5);
- const userChart = top5Users.map(([key, value]) => ({ name: key, value: value }));
-
+ const userChart = top5Users.map(([key, value]) => ({
+ name: key,
+ value: value,
+ }));
+
setData(arrayBarChart);
setUserData(userChart);
console.log("arrayBarChart:", arrayBarChart);
@@ -112,11 +153,10 @@ const ViewKeySpendReport: React.FC = ({ token, accessTo
}
};
- useEffect(() => {
- // Fetch data only when the token changes
- fetchData();
- }, [token]); // Dependency array containing the 'token' variable
-
+ // useEffect(() => {
+ // // Fetch data only when the token changes
+ // fetchData();
+ // }, [token]); // Dependency array containing the 'token' variable
if (!token) {
return null;
@@ -134,33 +174,28 @@ const ViewKeySpendReport: React.FC = ({ token, accessTo
onCancel={handleCancel}
footer={null}
>
- Key Name: {keyName}
+ Key Name: {keyName}
Monthly Spend ${keySpend}
- {data && (
+ {data && (
- )}
-
- Top 5 Users Spend (USD)
-
- {userData && (
-
- )}
-
-
+ )}
+
+ Top 5 Users Spend (USD)
+
+ {userData && (
+
+ )}
+
);
diff --git a/ui/litellm-dashboard/src/components/view_key_table.tsx b/ui/litellm-dashboard/src/components/view_key_table.tsx
index 8522a6bb1..4813bbe4e 100644
--- a/ui/litellm-dashboard/src/components/view_key_table.tsx
+++ b/ui/litellm-dashboard/src/components/view_key_table.tsx
@@ -1,5 +1,5 @@
"use client";
-import React, { useEffect } from "react";
+import React, { useEffect, useState } from "react";
import { keyDeleteCall } from "./networking";
import { StatusOnlineIcon, TrashIcon } from "@heroicons/react/outline";
import {
@@ -32,6 +32,8 @@ const ViewKeyTable: React.FC = ({
data,
setData,
}) => {
+ const [isButtonClicked, setIsButtonClicked] = useState(false);
+
const handleDelete = async (token: String) => {
if (data == null) {
return;
@@ -116,8 +118,13 @@ const ViewKeyTable: React.FC = ({
/>
-
-
+
);