diff --git a/.circleci/config.yml b/.circleci/config.yml index 2ee3f24a08dd..acf8612eacd9 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -105,7 +105,7 @@ jobs: command: | pwd ls - python -m pytest -vv tests/local_testing --cov=litellm --cov-report=xml -x --junitxml=test-results/junit.xml --durations=5 -k "not test_python_38.py and not router and not assistants and not langfuse and not caching and not cache" -n 4 + python -m pytest -vv tests/local_testing --cov=litellm --cov-report=xml -x --junitxml=test-results/junit.xml --durations=5 -k "not test_python_38.py and not test_basic_python_version.py and not router and not assistants and not langfuse and not caching and not cache" -n 4 no_output_timeout: 120m - run: name: Rename the coverage files @@ -895,6 +895,7 @@ jobs: pip install "pytest-retry==1.6.3" pip install "pytest-asyncio==0.21.1" pip install "pytest-cov==5.0.0" + pip install "tomli==2.2.1" - run: name: Run tests command: | diff --git a/README.md b/README.md index b1e6c9db3843..c7ea44cf462e 100644 --- a/README.md +++ b/README.md @@ -358,7 +358,7 @@ poetry install -E extra_proxy -E proxy Step 3: Test your change: ``` -cd litellm/tests # pwd: Documents/litellm/litellm/tests +cd tests # pwd: Documents/litellm/litellm/tests poetry run flake8 poetry run pytest . ``` diff --git a/dist/litellm-1.57.6.tar.gz b/dist/litellm-1.57.6.tar.gz new file mode 100644 index 000000000000..01a039cf6eea Binary files /dev/null and b/dist/litellm-1.57.6.tar.gz differ diff --git a/docs/my-website/docs/secret.md b/docs/my-website/docs/secret.md index 22bad6fec18b..a65c696f367b 100644 --- a/docs/my-website/docs/secret.md +++ b/docs/my-website/docs/secret.md @@ -72,6 +72,20 @@ general_settings: prefix_for_stored_virtual_keys: "litellm/" # OPTIONAL. If set, this prefix will be used for stored virtual keys in the secret manager access_mode: "write_only" # Literal["read_only", "write_only", "read_and_write"] ``` +</TabItem> +<TabItem value="read_and_write" label="Read + Write Keys with AWS Secret Manager"> + +```yaml +general_settings: + master_key: os.environ/litellm_master_key + key_management_system: "aws_secret_manager" # 👈 KEY CHANGE + key_management_settings: + store_virtual_keys: true # OPTIONAL. Defaults to False, when True will store virtual keys in secret manager + prefix_for_stored_virtual_keys: "litellm/" # OPTIONAL. If set, this prefix will be used for stored virtual keys in the secret manager + access_mode: "read_and_write" # Literal["read_only", "write_only", "read_and_write"] + hosted_keys: ["litellm_master_key"] # OPTIONAL. Specify which env keys you stored on AWS +``` + </TabItem> </Tabs> @@ -186,34 +200,6 @@ LiteLLM stores secret under the `prefix_for_stored_virtual_keys` path (default: ## Azure Key Vault -<!-- -### Quick Start - -```python -### Instantiate Azure Key Vault Client ### -from azure.keyvault.secrets import SecretClient -from azure.identity import ClientSecretCredential - -# Set your Azure Key Vault URI -KVUri = os.getenv("AZURE_KEY_VAULT_URI") - -# Set your Azure AD application/client ID, client secret, and tenant ID - create an application with permission to call your key vault -client_id = os.getenv("AZURE_CLIENT_ID") -client_secret = os.getenv("AZURE_CLIENT_SECRET") -tenant_id = os.getenv("AZURE_TENANT_ID") - -# Initialize the ClientSecretCredential -credential = ClientSecretCredential(client_id=client_id, client_secret=client_secret, tenant_id=tenant_id) - -# Create the SecretClient using the credential -client = SecretClient(vault_url=KVUri, credential=credential) - -### Connect to LiteLLM ### -import litellm -litellm.secret_manager = client - -litellm.get_secret("your-test-key") -``` --> #### Usage with LiteLLM Proxy Server diff --git a/litellm/proxy/_experimental/out/404.html b/litellm/proxy/_experimental/out/404.html deleted file mode 100644 index 3bbcc888404d..000000000000 --- a/litellm/proxy/_experimental/out/404.html +++ /dev/null @@ -1 +0,0 @@ -<!DOCTYPE html><html lang="en"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="preload" href="/ui/_next/static/media/a34f9d1faa5f3315-s.p.woff2" as="font" crossorigin="" type="font/woff2"/><link rel="stylesheet" href="/ui/_next/static/css/86f6cc749f6b8493.css" data-precedence="next"/><link rel="stylesheet" href="/ui/_next/static/css/08bcb9dd1e7e65fa.css" data-precedence="next"/><link rel="preload" as="script" fetchPriority="low" href="/ui/_next/static/chunks/webpack-75a5453f51d60261.js"/><script src="/ui/_next/static/chunks/fd9d1056-896c4ac5495ec10d.js" async=""></script><script src="/ui/_next/static/chunks/117-7dac20504d3a26dd.js" async=""></script><script src="/ui/_next/static/chunks/main-app-4f7318ae681a6d94.js" async=""></script><meta name="robots" content="noindex"/><title>404: This page could not be found.</title><title>LiteLLM Dashboard</title><meta name="description" content="LiteLLM Proxy Admin UI"/><meta name="next-size-adjust"/><script src="/ui/_next/static/chunks/polyfills-42372ed130431b0a.js" noModule=""></script></head><body class="__className_cf7686"><div style="font-family:system-ui,"Segoe UI",Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji";height:100vh;text-align:center;display:flex;flex-direction:column;align-items:center;justify-content:center"><div><style>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)}}</style><h1 class="next-error-h1" style="display:inline-block;margin:0 20px 0 0;padding:0 23px 0 0;font-size:24px;font-weight:500;vertical-align:top;line-height:49px">404</h1><div style="display:inline-block"><h2 style="font-size:14px;font-weight:400;line-height:49px;margin:0">This page could not be found.</h2></div></div></div><script src="/ui/_next/static/chunks/webpack-75a5453f51d60261.js" async=""></script><script>(self.__next_f=self.__next_f||[]).push([0]);self.__next_f.push([2,null])</script><script>self.__next_f.push([1,"1:HL[\"/ui/_next/static/media/a34f9d1faa5f3315-s.p.woff2\",\"font\",{\"crossOrigin\":\"\",\"type\":\"font/woff2\"}]\n2:HL[\"/ui/_next/static/css/86f6cc749f6b8493.css\",\"style\"]\n3:HL[\"/ui/_next/static/css/08bcb9dd1e7e65fa.css\",\"style\"]\n"])</script><script>self.__next_f.push([1,"4:I[12846,[],\"\"]\n6:I[4707,[],\"\"]\n7:I[36423,[],\"\"]\nd:I[61060,[],\"\"]\n8:{\"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\"}\n9:{\"display\":\"inline-block\",\"margin\":\"0 20px 0 0\",\"padding\":\"0 23px 0 0\",\"fontSize\":24,\"fontWeight\":500,\"verticalAlign\":\"top\",\"lineHeight\":\"49px\"}\na:{\"display\":\"inline-block\"}\nb:{\"fontSize\":14,\"fontWeight\":400,\"lineHeight\":\"49px\",\"margin\":0}\ne:[]\n"])</script><script>self.__next_f.push([1,"0:[\"$\",\"$L4\",null,{\"buildId\":\"cqOIY3Hj19kDs5fgiHCMl\",\"assetPrefix\":\"/ui\",\"urlParts\":[\"\",\"_not-found\"],\"initialTree\":[\"\",{\"children\":[\"/_not-found\",{\"children\":[\"__PAGE__\",{}]}]},\"$undefined\",\"$undefined\",true],\"initialSeedData\":[\"\",{\"children\":[\"/_not-found\",{\"children\":[\"__PAGE__\",{},[[\"$L5\",[[\"$\",\"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.\"}]}]]}]}]],null],null],null]},[null,[\"$\",\"$L6\",null,{\"parallelRouterKey\":\"children\",\"segmentPath\":[\"children\",\"/_not-found\",\"children\"],\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L7\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":\"$undefined\",\"notFoundStyles\":\"$undefined\"}]],null]},[[[[\"$\",\"link\",\"0\",{\"rel\":\"stylesheet\",\"href\":\"/ui/_next/static/css/86f6cc749f6b8493.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\"}],[\"$\",\"link\",\"1\",{\"rel\":\"stylesheet\",\"href\":\"/ui/_next/static/css/08bcb9dd1e7e65fa.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\"}]],[\"$\",\"html\",null,{\"lang\":\"en\",\"children\":[\"$\",\"body\",null,{\"className\":\"__className_cf7686\",\"children\":[\"$\",\"$L6\",null,{\"parallelRouterKey\":\"children\",\"segmentPath\":[\"children\"],\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L7\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":[[\"$\",\"title\",null,{\"children\":\"404: This page could not be found.\"}],[\"$\",\"div\",null,{\"style\":\"$8\",\"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\":\"$9\",\"children\":\"404\"}],[\"$\",\"div\",null,{\"style\":\"$a\",\"children\":[\"$\",\"h2\",null,{\"style\":\"$b\",\"children\":\"This page could not be found.\"}]}]]}]}]],\"notFoundStyles\":[]}]}]}]],null],null],\"couldBeIntercepted\":false,\"initialHead\":[[\"$\",\"meta\",null,{\"name\":\"robots\",\"content\":\"noindex\"}],\"$Lc\"],\"globalErrorComponent\":\"$d\",\"missingSlots\":\"$We\"}]\n"])</script><script>self.__next_f.push([1,"c:[[\"$\",\"meta\",\"0\",{\"name\":\"viewport\",\"content\":\"width=device-width, initial-scale=1\"}],[\"$\",\"meta\",\"1\",{\"charSet\":\"utf-8\"}],[\"$\",\"title\",\"2\",{\"children\":\"LiteLLM Dashboard\"}],[\"$\",\"meta\",\"3\",{\"name\":\"description\",\"content\":\"LiteLLM Proxy Admin UI\"}],[\"$\",\"meta\",\"4\",{\"name\":\"next-size-adjust\"}]]\n5:null\n"])</script></body></html> \ No newline at end of file diff --git a/litellm/proxy/_experimental/out/model_hub.html b/litellm/proxy/_experimental/out/model_hub.html deleted file mode 100644 index 8a63b4419fad..000000000000 --- a/litellm/proxy/_experimental/out/model_hub.html +++ /dev/null @@ -1 +0,0 @@ -<!DOCTYPE html><html id="__next_error__"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="preload" as="script" fetchPriority="low" href="/ui/_next/static/chunks/webpack-75a5453f51d60261.js"/><script src="/ui/_next/static/chunks/fd9d1056-896c4ac5495ec10d.js" async=""></script><script src="/ui/_next/static/chunks/117-7dac20504d3a26dd.js" async=""></script><script src="/ui/_next/static/chunks/main-app-4f7318ae681a6d94.js" async=""></script><title>LiteLLM Dashboard</title><meta name="description" content="LiteLLM Proxy Admin UI"/><link rel="icon" href="/ui/favicon.ico" type="image/x-icon" sizes="16x16"/><meta name="next-size-adjust"/><script src="/ui/_next/static/chunks/polyfills-42372ed130431b0a.js" noModule=""></script></head><body><script src="/ui/_next/static/chunks/webpack-75a5453f51d60261.js" async=""></script><script>(self.__next_f=self.__next_f||[]).push([0]);self.__next_f.push([2,null])</script><script>self.__next_f.push([1,"1:HL[\"/ui/_next/static/media/a34f9d1faa5f3315-s.p.woff2\",\"font\",{\"crossOrigin\":\"\",\"type\":\"font/woff2\"}]\n2:HL[\"/ui/_next/static/css/86f6cc749f6b8493.css\",\"style\"]\n3:HL[\"/ui/_next/static/css/08bcb9dd1e7e65fa.css\",\"style\"]\n"])</script><script>self.__next_f.push([1,"4:I[12846,[],\"\"]\n6:I[19107,[],\"ClientPageRoot\"]\n7:I[52829,[\"42\",\"static/chunks/42-4c9a56afdf4d8d5b.js\",\"261\",\"static/chunks/261-43203500d1424c9b.js\",\"250\",\"static/chunks/250-d54039b9fc5c0e65.js\",\"699\",\"static/chunks/699-9b535c39d02fc7c5.js\",\"418\",\"static/chunks/app/model_hub/page-3fcdb9cdde4e72fa.js\"],\"default\",1]\n8:I[4707,[],\"\"]\n9:I[36423,[],\"\"]\nb:I[61060,[],\"\"]\nc:[]\n"])</script><script>self.__next_f.push([1,"0:[\"$\",\"$L4\",null,{\"buildId\":\"cqOIY3Hj19kDs5fgiHCMl\",\"assetPrefix\":\"/ui\",\"urlParts\":[\"\",\"model_hub\"],\"initialTree\":[\"\",{\"children\":[\"model_hub\",{\"children\":[\"__PAGE__\",{}]}]},\"$undefined\",\"$undefined\",true],\"initialSeedData\":[\"\",{\"children\":[\"model_hub\",{\"children\":[\"__PAGE__\",{},[[\"$L5\",[\"$\",\"$L6\",null,{\"props\":{\"params\":{},\"searchParams\":{}},\"Component\":\"$7\"}],null],null],null]},[null,[\"$\",\"$L8\",null,{\"parallelRouterKey\":\"children\",\"segmentPath\":[\"children\",\"model_hub\",\"children\"],\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L9\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":\"$undefined\",\"notFoundStyles\":\"$undefined\"}]],null]},[[[[\"$\",\"link\",\"0\",{\"rel\":\"stylesheet\",\"href\":\"/ui/_next/static/css/86f6cc749f6b8493.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\"}],[\"$\",\"link\",\"1\",{\"rel\":\"stylesheet\",\"href\":\"/ui/_next/static/css/08bcb9dd1e7e65fa.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\"}]],[\"$\",\"html\",null,{\"lang\":\"en\",\"children\":[\"$\",\"body\",null,{\"className\":\"__className_cf7686\",\"children\":[\"$\",\"$L8\",null,{\"parallelRouterKey\":\"children\",\"segmentPath\":[\"children\"],\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L9\",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\":[]}]}]}]],null],null],\"couldBeIntercepted\":false,\"initialHead\":[null,\"$La\"],\"globalErrorComponent\":\"$b\",\"missingSlots\":\"$Wc\"}]\n"])</script><script>self.__next_f.push([1,"a:[[\"$\",\"meta\",\"0\",{\"name\":\"viewport\",\"content\":\"width=device-width, initial-scale=1\"}],[\"$\",\"meta\",\"1\",{\"charSet\":\"utf-8\"}],[\"$\",\"title\",\"2\",{\"children\":\"LiteLLM Dashboard\"}],[\"$\",\"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\"}]]\n5:null\n"])</script></body></html> \ No newline at end of file diff --git a/litellm/proxy/_experimental/out/onboarding.html b/litellm/proxy/_experimental/out/onboarding.html deleted file mode 100644 index 070f19d8e3f5..000000000000 --- a/litellm/proxy/_experimental/out/onboarding.html +++ /dev/null @@ -1 +0,0 @@ -<!DOCTYPE html><html id="__next_error__"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="preload" as="script" fetchPriority="low" href="/ui/_next/static/chunks/webpack-75a5453f51d60261.js"/><script src="/ui/_next/static/chunks/fd9d1056-896c4ac5495ec10d.js" async=""></script><script src="/ui/_next/static/chunks/117-7dac20504d3a26dd.js" async=""></script><script src="/ui/_next/static/chunks/main-app-4f7318ae681a6d94.js" async=""></script><title>LiteLLM Dashboard</title><meta name="description" content="LiteLLM Proxy Admin UI"/><link rel="icon" href="/ui/favicon.ico" type="image/x-icon" sizes="16x16"/><meta name="next-size-adjust"/><script src="/ui/_next/static/chunks/polyfills-42372ed130431b0a.js" noModule=""></script></head><body><script src="/ui/_next/static/chunks/webpack-75a5453f51d60261.js" async=""></script><script>(self.__next_f=self.__next_f||[]).push([0]);self.__next_f.push([2,null])</script><script>self.__next_f.push([1,"1:HL[\"/ui/_next/static/media/a34f9d1faa5f3315-s.p.woff2\",\"font\",{\"crossOrigin\":\"\",\"type\":\"font/woff2\"}]\n2:HL[\"/ui/_next/static/css/86f6cc749f6b8493.css\",\"style\"]\n3:HL[\"/ui/_next/static/css/08bcb9dd1e7e65fa.css\",\"style\"]\n"])</script><script>self.__next_f.push([1,"4:I[12846,[],\"\"]\n6:I[19107,[],\"ClientPageRoot\"]\n7:I[12011,[\"665\",\"static/chunks/3014691f-879b58d3fc1547e5.js\",\"42\",\"static/chunks/42-4c9a56afdf4d8d5b.js\",\"755\",\"static/chunks/755-c7e0ae255f32cb18.js\",\"250\",\"static/chunks/250-d54039b9fc5c0e65.js\",\"461\",\"static/chunks/app/onboarding/page-786f929a4f77e0e6.js\"],\"default\",1]\n8:I[4707,[],\"\"]\n9:I[36423,[],\"\"]\nb:I[61060,[],\"\"]\nc:[]\n"])</script><script>self.__next_f.push([1,"0:[\"$\",\"$L4\",null,{\"buildId\":\"cqOIY3Hj19kDs5fgiHCMl\",\"assetPrefix\":\"/ui\",\"urlParts\":[\"\",\"onboarding\"],\"initialTree\":[\"\",{\"children\":[\"onboarding\",{\"children\":[\"__PAGE__\",{}]}]},\"$undefined\",\"$undefined\",true],\"initialSeedData\":[\"\",{\"children\":[\"onboarding\",{\"children\":[\"__PAGE__\",{},[[\"$L5\",[\"$\",\"$L6\",null,{\"props\":{\"params\":{},\"searchParams\":{}},\"Component\":\"$7\"}],null],null],null]},[null,[\"$\",\"$L8\",null,{\"parallelRouterKey\":\"children\",\"segmentPath\":[\"children\",\"onboarding\",\"children\"],\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L9\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":\"$undefined\",\"notFoundStyles\":\"$undefined\"}]],null]},[[[[\"$\",\"link\",\"0\",{\"rel\":\"stylesheet\",\"href\":\"/ui/_next/static/css/86f6cc749f6b8493.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\"}],[\"$\",\"link\",\"1\",{\"rel\":\"stylesheet\",\"href\":\"/ui/_next/static/css/08bcb9dd1e7e65fa.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\"}]],[\"$\",\"html\",null,{\"lang\":\"en\",\"children\":[\"$\",\"body\",null,{\"className\":\"__className_cf7686\",\"children\":[\"$\",\"$L8\",null,{\"parallelRouterKey\":\"children\",\"segmentPath\":[\"children\"],\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L9\",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\":[]}]}]}]],null],null],\"couldBeIntercepted\":false,\"initialHead\":[null,\"$La\"],\"globalErrorComponent\":\"$b\",\"missingSlots\":\"$Wc\"}]\n"])</script><script>self.__next_f.push([1,"a:[[\"$\",\"meta\",\"0\",{\"name\":\"viewport\",\"content\":\"width=device-width, initial-scale=1\"}],[\"$\",\"meta\",\"1\",{\"charSet\":\"utf-8\"}],[\"$\",\"title\",\"2\",{\"children\":\"LiteLLM Dashboard\"}],[\"$\",\"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\"}]]\n5:null\n"])</script></body></html> \ No newline at end of file diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index d08105988d7a..c5fe5518786d 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -2101,12 +2101,13 @@ class TeamMemberDeleteRequest(MemberDeleteRequest): class TeamMemberUpdateRequest(TeamMemberDeleteRequest): - max_budget_in_team: float + max_budget_in_team: Optional[float] = None + role: Optional[Literal["admin", "user"]] = None class TeamMemberUpdateResponse(MemberUpdateResponse): team_id: str - max_budget_in_team: float + max_budget_in_team: Optional[float] = None # Organization Member Requests diff --git a/litellm/proxy/management_endpoints/team_endpoints.py b/litellm/proxy/management_endpoints/team_endpoints.py index 1d21e315b37a..6ebfed0eef11 100644 --- a/litellm/proxy/management_endpoints/team_endpoints.py +++ b/litellm/proxy/management_endpoints/team_endpoints.py @@ -920,7 +920,7 @@ async def team_member_update( """ [BETA] - Update team member budgets + Update team member budgets and team member role """ from litellm.proxy.proxy_server import prisma_client @@ -970,6 +970,8 @@ async def team_member_update( user_api_key_dict=user_api_key_dict, ) + team_table = returned_team_info["team_info"] + ## get user id received_user_id: Optional[str] = None if data.user_id is not None: @@ -995,26 +997,50 @@ async def team_member_update( break ### upsert new budget - if identified_budget_id is None: - new_budget = await prisma_client.db.litellm_budgettable.create( - data={ - "max_budget": data.max_budget_in_team, - "created_by": user_api_key_dict.user_id or "", - "updated_by": user_api_key_dict.user_id or "", - } - ) + if data.max_budget_in_team is not None: + if identified_budget_id is None: + new_budget = await prisma_client.db.litellm_budgettable.create( + data={ + "max_budget": data.max_budget_in_team, + "created_by": user_api_key_dict.user_id or "", + "updated_by": user_api_key_dict.user_id or "", + } + ) - await prisma_client.db.litellm_teammembership.create( - data={ - "team_id": data.team_id, - "user_id": received_user_id, - "budget_id": new_budget.budget_id, - }, - ) - else: - await prisma_client.db.litellm_budgettable.update( - where={"budget_id": identified_budget_id}, - data={"max_budget": data.max_budget_in_team}, + await prisma_client.db.litellm_teammembership.create( + data={ + "team_id": data.team_id, + "user_id": received_user_id, + "budget_id": new_budget.budget_id, + }, + ) + elif identified_budget_id is not None: + await prisma_client.db.litellm_budgettable.update( + where={"budget_id": identified_budget_id}, + data={"max_budget": data.max_budget_in_team}, + ) + + ### update team member role + if data.role is not None: + team_members: List[Member] = [] + for member in team_table.members_with_roles: + if member.user_id == received_user_id: + team_members.append( + Member( + user_id=member.user_id, + role=data.role, + user_email=data.user_email or member.user_email, + ) + ) + else: + team_members.append(member) + + team_table.members_with_roles = team_members + + _db_team_members: List[dict] = [m.model_dump() for m in team_members] + await prisma_client.db.litellm_teamtable.update( + where={"team_id": data.team_id}, + data={"members_with_roles": json.dumps(_db_team_members)}, # type: ignore ) return TeamMemberUpdateResponse( diff --git a/tests/local_testing/test_basic_python_version.py b/tests/local_testing/test_basic_python_version.py index 5fa48f09699d..c629ef3df8e9 100644 --- a/tests/local_testing/test_basic_python_version.py +++ b/tests/local_testing/test_basic_python_version.py @@ -37,6 +37,51 @@ def test_litellm_proxy_server(): assert True +def test_package_dependencies(): + try: + import tomli + import pathlib + import litellm + + # Get the litellm package root path + litellm_path = pathlib.Path(litellm.__file__).parent.parent + pyproject_path = litellm_path / "pyproject.toml" + + # Read and parse pyproject.toml + with open(pyproject_path, "rb") as f: + pyproject = tomli.load(f) + + # Get all optional dependencies from poetry.dependencies + poetry_deps = pyproject["tool"]["poetry"]["dependencies"] + optional_deps = { + name.lower() + for name, value in poetry_deps.items() + if isinstance(value, dict) and value.get("optional", False) + } + print(optional_deps) + # Get all packages listed in extras + extras = pyproject["tool"]["poetry"]["extras"] + all_extra_deps = set() + for extra_group in extras.values(): + all_extra_deps.update(dep.lower() for dep in extra_group) + print(all_extra_deps) + # Check that all optional dependencies are in some extras group + missing_from_extras = optional_deps - all_extra_deps + assert ( + not missing_from_extras + ), f"Optional dependencies missing from extras: {missing_from_extras}" + + print( + f"All {len(optional_deps)} optional dependencies are correctly specified in extras" + ) + + except Exception as e: + pytest.fail( + f"Error occurred while checking dependencies: {str(e)}\n" + + traceback.format_exc() + ) + + import os import subprocess import time diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx index 75c50727576c..159bc035d196 100644 --- a/ui/litellm-dashboard/src/components/networking.tsx +++ b/ui/litellm-dashboard/src/components/networking.tsx @@ -2321,6 +2321,47 @@ export const teamMemberAddCall = async ( } }; +export const teamMemberUpdateCall = async ( + accessToken: string, + teamId: string, + formValues: Member // Assuming formValues is an object +) => { + try { + console.log("Form Values in teamMemberAddCall:", formValues); // Log the form values before making the API call + + const url = proxyBaseUrl + ? `${proxyBaseUrl}/team/member_update` + : `/team/member_update`; + const response = await fetch(url, { + method: "POST", + headers: { + [globalLitellmHeaderName]: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + team_id: teamId, + role: formValues.role, + user_id: formValues.user_id, + }), + }); + + if (!response.ok) { + const errorData = await response.text(); + handleError(errorData); + console.error("Error response from the server:", errorData); + throw new Error("Network response was not ok"); + } + + const data = await response.json(); + console.log("API Response:", 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; + } +} + export const organizationMemberAddCall = async ( accessToken: string, organizationId: string, diff --git a/ui/litellm-dashboard/src/components/teams.tsx b/ui/litellm-dashboard/src/components/teams.tsx index 8979a96a3b4c..e988b65e3a70 100644 --- a/ui/litellm-dashboard/src/components/teams.tsx +++ b/ui/litellm-dashboard/src/components/teams.tsx @@ -2,6 +2,7 @@ import React, { useState, useEffect } from "react"; import Link from "next/link"; import { Typography } from "antd"; import { teamDeleteCall, teamUpdateCall, teamInfoCall } from "./networking"; +import TeamMemberModal, { TeamMember } from "@/components/team/edit_membership"; import { InformationCircleIcon, PencilAltIcon, @@ -65,12 +66,12 @@ interface EditTeamModalProps { import { teamCreateCall, teamMemberAddCall, + teamMemberUpdateCall, Member, modelAvailableCall, teamListCall } from "./networking"; - const Team: React.FC<TeamProps> = ({ teams, searchParams, @@ -112,11 +113,12 @@ const Team: React.FC<TeamProps> = ({ const [isTeamModalVisible, setIsTeamModalVisible] = useState(false); const [isAddMemberModalVisible, setIsAddMemberModalVisible] = useState(false); + const [isEditMemberModalVisible, setIsEditMemberModalVisible] = useState(false); const [userModels, setUserModels] = useState([]); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [teamToDelete, setTeamToDelete] = useState<string | null>(null); + const [selectedEditMember, setSelectedEditMember] = useState<null | TeamMember>(null); - // store team info as {"team_id": team_info_object} const [perTeamInfo, setPerTeamInfo] = useState<Record<string, any>>({}); const EditTeamModal: React.FC<EditTeamModalProps> = ({ @@ -257,16 +259,19 @@ const Team: React.FC<TeamProps> = ({ const handleMemberOk = () => { setIsAddMemberModalVisible(false); + setIsEditMemberModalVisible(false); memberForm.resetFields(); }; const handleCancel = () => { setIsTeamModalVisible(false); + form.resetFields(); }; const handleMemberCancel = () => { setIsAddMemberModalVisible(false); + setIsEditMemberModalVisible(false); memberForm.resetFields(); }; @@ -412,7 +417,7 @@ const Team: React.FC<TeamProps> = ({ return false; } - const handleMemberCreate = async (formValues: Record<string, any>) => { + const _common_member_update_call = async (formValues: Record<string, any>, callType: "add" | "edit") => { try { if (accessToken != null && teams != null) { message.info("Adding Member"); @@ -421,13 +426,27 @@ const Team: React.FC<TeamProps> = ({ user_email: formValues.user_email, user_id: formValues.user_id, }; - const response: any = await teamMemberAddCall( - accessToken, - selectedTeam["team_id"], - user_role - ); - message.success("Member added"); - console.log(`response for team create call: ${response["data"]}`); + let response: any; + if (callType == "add") { + response = await teamMemberAddCall( + accessToken, + selectedTeam["team_id"], + user_role + ); + message.success("Member added"); + } else { + response = await teamMemberUpdateCall( + accessToken, + selectedTeam["team_id"], + { + "role": formValues.role, + "user_id": formValues.id, + "user_email": formValues.email + } + ); + message.success("Member updated"); + } + // Checking if the team exists in the list and updating or adding accordingly const foundIndex = teams.findIndex((team) => { console.log( @@ -449,7 +468,15 @@ const Team: React.FC<TeamProps> = ({ } catch (error) { console.error("Error creating the team:", error); } + } + + const handleMemberCreate = async (formValues: Record<string, any>) => { + _common_member_update_call(formValues, "add"); }; + + const handleMemberUpdate = async (formValues: Record<string, any>) => { + _common_member_update_call(formValues, "edit"); + } return ( <div className="w-full mx-4"> <Grid numItems={1} className="gap-2 p-8 h-[75vh] w-full mt-2"> @@ -831,6 +858,29 @@ const Team: React.FC<TeamProps> = ({ : null} </TableCell> <TableCell>{member["role"]}</TableCell> + <TableCell> + {userRole == "Admin" ? ( + <> + <Icon + icon={PencilAltIcon} + size="sm" + onClick={() => { + setIsEditMemberModalVisible(true); + setSelectedEditMember({ + "id": member["user_id"], + "email": member["user_email"], + "role": member["role"] + }) + }} + /> + <Icon + onClick={() => {}} + icon={TrashIcon} + size="sm" + /> + </> + ) : null} + </TableCell> </TableRow> ) ) @@ -838,6 +888,13 @@ const Team: React.FC<TeamProps> = ({ </TableBody> </Table> </Card> + <TeamMemberModal + visible={isEditMemberModalVisible} + onCancel={handleMemberCancel} + onSubmit={handleMemberUpdate} + initialData={selectedEditMember} + mode="edit" + /> {selectedTeam && ( <EditTeamModal visible={editModalVisible} diff --git a/ui/litellm-dashboard/src/components/user_dashboard.tsx b/ui/litellm-dashboard/src/components/user_dashboard.tsx index cb5055100e93..e00f36023270 100644 --- a/ui/litellm-dashboard/src/components/user_dashboard.tsx +++ b/ui/litellm-dashboard/src/components/user_dashboard.tsx @@ -5,6 +5,7 @@ import { modelAvailableCall, getTotalSpendCall, getProxyUISettings, + teamListCall, } from "./networking"; import { Grid, Col, Card, Text, Title } from "@tremor/react"; import CreateKey from "./create_key_button"; @@ -172,6 +173,18 @@ const UserDashboard: React.FC<UserDashboardProps> = ({ if (cachedUserModels) { setUserModels(JSON.parse(cachedUserModels)); } else { + const fetchTeams = async () => { + let givenTeams; + if (userRole != "Admin" && userRole != "Admin Viewer") { + givenTeams = await teamListCall(accessToken, userID) + } else { + givenTeams = await teamListCall(accessToken) + } + + console.log(`givenTeams: ${givenTeams}`) + + setTeams(givenTeams) + } const fetchData = async () => { try { const proxy_settings: ProxySettings = await getProxyUISettings(accessToken); @@ -193,7 +206,6 @@ const UserDashboard: React.FC<UserDashboardProps> = ({ setUserSpendData(response["user_info"]); console.log(`userSpendData: ${JSON.stringify(userSpendData)}`) - const teamsArray = [...response["teams"]]; if (teamsArray.length > 0) { console.log(`response['teams']: ${JSON.stringify(teamsArray)}`); @@ -236,6 +248,7 @@ const UserDashboard: React.FC<UserDashboardProps> = ({ } }; fetchData(); + fetchTeams(); } } }, [userID, token, accessToken, keys, userRole]); diff --git a/ui/litellm-dashboard/src/components/view_key_table.tsx b/ui/litellm-dashboard/src/components/view_key_table.tsx index 9cc417601f68..184c817faae2 100644 --- a/ui/litellm-dashboard/src/components/view_key_table.tsx +++ b/ui/litellm-dashboard/src/components/view_key_table.tsx @@ -133,6 +133,58 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({ const [knownTeamIDs, setKnownTeamIDs] = useState(initialKnownTeamIDs); const [guardrailsList, setGuardrailsList] = useState<string[]>([]); + // Function to check if user is admin of a team + const isUserTeamAdmin = (team: any) => { + if (!team.members_with_roles) return false; + return team.members_with_roles.some( + (member: any) => member.role === "admin" && member.user_id === userID + ); + }; + + // Combine all keys that user should have access to + const all_keys_to_display = React.useMemo(() => { + let allKeys: any[] = []; + + // If no teams, return personal keys + if (!teams || teams.length === 0) { + return data; + } + + teams.forEach(team => { + // For default team or when user is not admin, use personal keys (data) + if (team.team_id === "default-team" || !isUserTeamAdmin(team)) { + if (selectedTeam && selectedTeam.team_id === team.team_id && data) { + allKeys = [...allKeys, ...data.filter(key => key.team_id === team.team_id)]; + } + } + // For teams where user is admin, use team keys + else if (isUserTeamAdmin(team)) { + if (selectedTeam && selectedTeam.team_id === team.team_id) { + allKeys = [...allKeys, ...(team.keys || [])]; + } + } + }); + + // If no team is selected, show all accessible keys + if (!selectedTeam && data) { + const personalKeys = data.filter(key => !key.team_id || key.team_id === "default-team"); + const adminTeamKeys = teams + .filter(team => isUserTeamAdmin(team)) + .flatMap(team => team.keys || []); + allKeys = [...personalKeys, ...adminTeamKeys]; + } + + // Filter out litellm-dashboard keys + allKeys = allKeys.filter(key => key.team_id !== "litellm-dashboard"); + + // Remove duplicates based on token + const uniqueKeys = Array.from( + new Map(allKeys.map(key => [key.token, key])).values() + ); + + return uniqueKeys; + }, [data, teams, selectedTeam, userID]); + useEffect(() => { const calculateNewExpiryTime = (duration: string | undefined) => { if (!duration) { @@ -901,7 +953,7 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({ </TableRow> </TableHead> <TableBody> - {data.map((item) => { + {all_keys_to_display && all_keys_to_display.map((item) => { console.log(item); // skip item if item.team_id == "litellm-dashboard" if (item.team_id === "litellm-dashboard") {