├── .gitignore ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── app ├── auth │ └── callback │ │ └── route.ts ├── globals.css ├── layout.tsx ├── login │ ├── page.tsx │ └── submit-button.tsx ├── page.tsx └── protected │ └── page.tsx ├── components ├── AuthButton.tsx └── Header.tsx ├── configs └── huggingface-tc-bert-base-cased-202403291810.yaml ├── example_configs ├── bert-eqa.yaml ├── bert-tc.yaml ├── llama7b.yaml └── train-bert.yaml ├── middleware.ts ├── model_manager.py ├── next.config.js ├── package-lock.json ├── package.json ├── pdm.lock ├── postcss.config.js ├── pyproject.toml ├── requirements.txt ├── scripts └── setup_role.sh ├── server.py ├── setup.sh ├── src ├── __init__.py ├── config.py ├── console.py ├── huggingface │ ├── __init__.py │ └── hf_hub_api.py ├── main.py ├── sagemaker │ ├── __init__.py │ ├── create_model.py │ ├── delete_model.py │ ├── fine_tune_model.py │ ├── query_endpoint.py │ ├── resources.py │ └── search_jumpstart_models.py ├── schemas │ ├── __init__.py │ ├── deployment.py │ ├── model.py │ ├── query.py │ └── training.py ├── session.py ├── utils │ ├── __init__.py │ ├── aws_utils.py │ ├── format.py │ ├── model_utils.py │ └── rich_utils.py └── yaml.py ├── tailwind.config.js ├── tests ├── __init__.py └── test_openai_proxy.py ├── tsconfig.json └── utils └── supabase ├── client.ts ├── middleware.ts └── server.ts /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | 38 | # Byte-compiled / optimized / DLL files 39 | __pycache__/ 40 | *.py[cod] 41 | *$py.class 42 | 43 | # C extensions 44 | *.so 45 | 46 | # Distribution / packaging 47 | .Python 48 | build/ 49 | develop-eggs/ 50 | dist/ 51 | downloads/ 52 | eggs/ 53 | .eggs/ 54 | lib/ 55 | lib64/ 56 | parts/ 57 | sdist/ 58 | var/ 59 | wheels/ 60 | share/python-wheels/ 61 | *.egg-info/ 62 | .installed.cfg 63 | *.egg 64 | MANIFEST 65 | 66 | # PyInstaller 67 | # Usually these files are written by a python script from a template 68 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 69 | *.manifest 70 | *.spec 71 | 72 | # Installer logs 73 | pip-log.txt 74 | pip-delete-this-directory.txt 75 | 76 | # Unit test / coverage reports 77 | htmlcov/ 78 | .tox/ 79 | .nox/ 80 | .coverage 81 | .coverage.* 82 | .cache 83 | nosetests.xml 84 | coverage.xml 85 | *.cover 86 | *.py,cover 87 | .hypothesis/ 88 | .pytest_cache/ 89 | cover/ 90 | 91 | # Translations 92 | *.mo 93 | *.pot 94 | 95 | # Django stuff: 96 | *.log 97 | local_settings.py 98 | db.sqlite3 99 | db.sqlite3-journal 100 | 101 | # Flask stuff: 102 | instance/ 103 | .webassets-cache 104 | 105 | # Scrapy stuff: 106 | .scrapy 107 | 108 | # Sphinx documentation 109 | docs/_build/ 110 | 111 | # PyBuilder 112 | .pybuilder/ 113 | target/ 114 | 115 | # Jupyter Notebook 116 | .ipynb_checkpoints 117 | 118 | # IPython 119 | profile_default/ 120 | ipython_config.py 121 | 122 | # pyenv 123 | # For a library or package, you might want to ignore these files since the code is 124 | # intended to run in multiple environments; otherwise, check them in: 125 | # .python-version 126 | 127 | # pipenv 128 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 129 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 130 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 131 | # install all needed dependencies. 132 | #Pipfile.lock 133 | 134 | # poetry 135 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 136 | # This is especially recommended for binary packages to ensure reproducibility, and is more 137 | # commonly ignored for libraries. 138 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 139 | #poetry.lock 140 | 141 | # pdm 142 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 143 | #pdm.lock 144 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 145 | # in version control. 146 | # https://pdm.fming.dev/#use-with-ide 147 | .pdm.toml 148 | 149 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 150 | __pypackages__/ 151 | 152 | # Celery stuff 153 | celerybeat-schedule 154 | celerybeat.pid 155 | 156 | # SageMath parsed files 157 | *.sage.py 158 | 159 | # Environments 160 | .env 161 | .venv 162 | env/ 163 | venv/ 164 | ENV/ 165 | env.bak/ 166 | venv.bak/ 167 | 168 | # Spyder project settings 169 | .spyderproject 170 | .spyproject 171 | 172 | # Rope project settings 173 | .ropeproject 174 | 175 | # mkdocs documentation 176 | /site 177 | 178 | # mypy 179 | .mypy_cache/ 180 | .dmypy.json 181 | dmypy.json 182 | 183 | # Pyre type checker 184 | .pyre/ 185 | 186 | # pytype static type analyzer 187 | .pytype/ 188 | 189 | # Cython debug symbols 190 | cython_debug/ 191 | 192 | # PyCharm 193 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 194 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 195 | # and can be added to the global gitignore or merged into this file. For a more nuclear 196 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 197 | #.idea/ 198 | 199 | # vscode 200 | .vscode/ 201 | # es-lint 202 | .eslintrc.cjs 203 | 204 | models/ 205 | configs/ 206 | .pdm-python 207 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Use an official Python runtime as a parent image 2 | FROM python:3.12-slim 3 | 4 | # Set environment variables 5 | ENV PYTHONDONTWRITEBYTECODE 1 6 | ENV PYTHONUNBUFFERED 1 7 | 8 | # Set the working directory in the container 9 | WORKDIR /app 10 | 11 | # Copy the project files to the working directory 12 | COPY . . 13 | 14 | # Install PDM and use it to install dependencies 15 | RUN pip install --no-cache-dir pdm \ 16 | && pdm install --no-interactive 17 | 18 | # Expose port 8000 to the outside world 19 | EXPOSE 8000 20 | 21 | # Run uvicorn when the container launches 22 | CMD ["pdm", "run", "uvicorn", "server:app", "--host", "0.0.0.0", "--port", "8000"] 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 OpenFoundry 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | venv: venv/touchfile 2 | 3 | venv/touchfile: requirements.txt 4 | test -d venv || python3 -m venv venv 5 | . venv/bin/activate; pip install -Ur requirements.txt 6 | touch venv/touchfile 7 | clean: 8 | rm -rf venv -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 |
7 |

Model Manager v0.1, by

8 | 9 | ![openfoundry white](https://github.com/openfoundry-ai/model_manager/assets/152243036/01b64d39-1402-45b6-8dce-8b4626c41935) 10 | 11 |

12 | Deploy open source AI models to AWS in minutes. 13 |
14 |

15 |
16 | 17 | 18 | 19 | 20 |
21 | Table of Contents 22 |
    23 |
  1. 24 | About Model Manager 25 |
  2. 26 |
  3. 27 | Getting Started 28 | 32 |
  4. 33 |
  5. Using Model Manager
  6. 34 |
  7. What we're working on next
  8. 35 |
  9. Known issues
  10. 36 |
  11. Contributing
  12. 37 |
  13. License
  14. 38 |
  15. Contact
  16. 39 |
40 |
41 | 42 | 43 | ## About Model Manager 44 | Model Manager is a Python tool that simplifies the process of deploying an open source AI model to your own cloud. Instead of spending hours digging through documentation to figure out how to get AWS working, Model Manager lets you deploy open source AI models directly from the command line. 45 | 46 | Choose a model from Hugging Face or SageMaker, and Model Manager will spin up a SageMaker instance with a ready-to-query endpoint in minutes. 47 | 48 | Here we’re deploying Microsoft’s Phi2. Larger models such as this take about 10 minutes to spin up. 49 | 50 | https://github.com/openfoundry-ai/model_manager/assets/164248540/f7fbf9ce-04c3-49b0-b5be-2977b5cc90af 51 | 52 |
53 | Once the model is running, you can query it to get a response. 54 | 55 | ![Screenshot 2024-03-20 at 6 01 44 PM](https://github.com/openfoundry-ai/model_manager/assets/164248540/20b46f8f-da01-4cc7-8343-e647b27ba7c6) 56 | 57 | 58 |

(back to top)

59 | 60 | 61 | 62 |
63 | 64 | ## Getting Started 65 | 66 | Model Manager works with AWS. Azure and GCP support are coming soon! 67 | To get a local copy up and running follow these simple steps. 68 | 69 | ### Prerequisites 70 | 71 | * Python 72 | * An AWS account 73 | * Quota for AWS SageMaker instances (by default, you get 2 instances of ml.m5.xlarge for free) 74 | * Certain Hugging Face models (e.g. Llama2) require an access token ([hf docs](https://huggingface.co/docs/hub/en/models-gated#access-gated-models-as-a-user)) 75 | 76 | 77 | ### Installation 78 | 79 | **Step 1: Set up AWS and SageMaker** 80 | 81 | To get started, you’ll need an AWS account which you can create at https://aws.amazon.com/. Then you’ll need to create access keys for SageMaker. 82 | 83 | We made a walkthrough video to show you how to get set up with your SageMaker access keys in 2 minutes. 84 | 85 | https://github.com/openfoundry-ai/model_manager/assets/164248540/52b0dcee-87cd-48de-9251-b2d3571abf61 86 | 87 | If you prefer a written doc, we wrote up the steps in [Google Doc](https://docs.google.com/document/d/1kLzPU43kvLAoYzmBfvAkINmXZ24JzAp_fdBIYjJRXvU/edit?usp=sharing) as well. 88 | 89 | 90 | **Step 2: Set up Model Manager** 91 | 92 | You should now have your Access Key and Secret from SageMaker. Now you can set up Model Manager! Clone the repo to your local machine, and then run the setup script in the repo: 93 | 94 | ```sh 95 | bash setup.sh 96 | ``` 97 | 98 | This will configure the AWS client so you’re ready to start deploying models. You’ll be prompted to enter your Access Key and Secret here. You can also specify your AWS region. The default is us-east-1. You only need to change this if your SageMaker instance quota is in a different region. 99 | 100 | Optional: If you have a Hugging Face Hub Token, you can add it to `.env` that was generated by the setup script and add it with the key: 101 | 102 | ```sh 103 | HUGGING_FACE_HUB_KEY="KeyValueHere" 104 | ``` 105 | 106 | This will allow you to use models with access restrictions such as Llama2 as long as your Hugging Face account has permission to do so. 107 | 108 | 109 |

(back to top)

110 | 111 | 112 | 113 | 114 |
115 | 116 | ## Using Model Manager 117 | After you’ve set up AWS and Model Manager per the above, run Model Manager using python or python3: 118 | ```sh 119 | python3 model_manager.py 120 | ``` 121 | 122 | ![home screen](https://github.com/openfoundry-ai/model_manager/assets/164248540/22ede5aa-99e3-4191-bca8-3021c6b572e7) 123 | 124 | 125 | Now you’re ready to start shipping models onto your cloud! 126 |
127 |
128 | 129 | ### Deploying models 130 | 131 | There are three ways from where you can deploy models: Hugging Face, SageMaker, or your own custom model. Use whichever works for you! If you're deploying with Hugging Face, copy/paste the full model name from Hugging Face. For example, `google-bert/bert-base-uncased`. Note that you’ll need larger, more expensive instance types in order to run bigger models. It takes anywhere from 2 minutes (for smaller models) to 10+ minutes (for large models) to spin up the instance with your model. If you are deploying a Sagemaker model, select a framework and search from a model. If you a deploying a custom model, provide either a valid S3 path or a local path (and the tool will automatically upload it for you). Once deployed, we will generate a YAML file with the deployment and model under `/configs` 132 | 133 | #### Deploy using a yaml file 134 | For future deploys, we recommend deploying through a yaml file for reproducability and IAC. From the cli, you can deploy a model without going through all the menus. You can even integrate us with your Github Actions to deploy on PR merge. Deploy via YAML files simply by passing the `--deploy` option with local path like so: 135 | ``` 136 | python model_manager.py --deploy ./example_configs/llama7b.yaml 137 | ``` 138 | 139 |
140 |
141 | 142 | If you’re using the `ml.m5.xlarge` instance type, here are some small Hugging Face models that work great: 143 |
144 |
145 | 146 | **Model: [google-bert/bert-base-uncased](https://huggingface.co/google-bert/bert-base-uncased)** 147 | 148 | - **Type:** Fill Mask: tries to complete your sentence like Madlibs 149 | - **Query format:** text string with `[MASK]` somewhere in it that you wish for the transformer to fill 150 | 151 | ![fill mask bert query](https://github.com/openfoundry-ai/model_manager/assets/164248540/a8a6d8e9-183f-4c85-afe8-b9c21d8aa687) 152 | 153 |
154 |
155 | 156 | **Model: [sentence-transformers/all-MiniLM-L6-v2](https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2)** 157 | 158 | - **Type:** Feature extraction: turns text into a 384d vector embedding for semantic search / clustering 159 | - **Query format:** "*type out a sentence like this one.*" 160 | 161 | ![sentence transformer query](https://github.com/openfoundry-ai/model_manager/assets/164248540/57b4da43-03bb-4642-892b-5f287dfce0d8) 162 | 163 |
164 |
165 | 166 | **Model: [deepset/roberta-base-squad2](https://huggingface.co/deepset/roberta-base-squad2)** 167 | 168 | - **Type:** Question answering; provide a question and some context from which the transformer will answer the question. 169 | - **Query format:** A dict with two keys: `question` and `context`. For our tool, we will prompt you a second time to provide the context. 170 | 171 | ![roberta eqa query](https://github.com/openfoundry-ai/model_manager/assets/164248540/2054fdb7-1f3d-4bfe-a806-c5ebde8ad20d) 172 | 173 |
174 |
175 | 176 | ### Querying models 177 | There are three ways to query a model you’ve deployed: you can query it using the Model Manager script, spin up a FastAPI server, or call it directly from your code using SageMaker’s API. 178 | 179 | To spin up a FastAPI server, run 180 | ``` 181 | uvicorn server:app --reload 182 | ``` 183 | This will create a server running at `0.0.0.0` on port 8000 which you can query against from your app. There are 2 endpoints: 184 | 1. `GET /endpoint/{endpoint_name}`: Get information about a deployed endpoint 185 | 2. `POST /endpoint/{endpoint_name}/query`: Query a model for inference. The request expects a JSON body with only the `query` key being required. `context` is required for some types of models (such as question-answering). `parameters` can be passed for text-generation/LLM models to further control the output of the model. 186 | ``` 187 | { 188 | "query": "string", 189 | "context": "string", 190 | "parameters": { 191 | "max_length": 0, 192 | "max_new_tokens": 0, 193 | "repetition_penalty": 0, 194 | "temperature": 0, 195 | "top_k": 0, 196 | "top_p": 0 197 | } 198 | } 199 | ``` 200 | 201 | Querying within Model Manager currently works for text-based models. Image generation, multi-modal, etc. models are not yet supported. 202 | 203 | You can query all deployed models using the SageMaker API. Documentation for how to do this can be found [here](https://docs.aws.amazon.com/sagemaker/latest/APIReference/API_runtime_InvokeEndpoint.html). 204 | 205 | ### Deactivating models 206 | 207 | Any model endpoints you spin up will run continuously unless you deactivate them! Make sure to delete endpoints you’re no longer using so you don’t keep getting charged for your SageMaker instance. 208 | 209 | 210 |

(back to top)

211 | 212 | 213 | 214 | 215 |
216 | 217 | ## What we're working on next 218 | - [ ] More robust error handling for various edge cases 219 | - [ ] Verbose logging 220 | - [ ] Enabling / disabling autoscaling 221 | - [ ] Deployment to Azure and GCP 222 | 223 |

(back to top)

224 | 225 | 226 | 227 |
228 | 229 | ## Known issues 230 | - [ ] Querying within Model Manager currently only works with text-based model - doesn’t work with multimodal, image generation, etc. 231 | - [ ] Model versions are static. 232 | - [ ] Deleting a model is not instant, it may show up briefly after it was queued for deletion 233 | - [ ] Deploying the same model within the same minute will break 234 | 235 | See [open issues](https://github.com/openfoundry-ai/model_manager/issues) for a full list of known issues and proposed features. 236 | 237 |

(back to top)

238 | 239 | 240 | 241 |
242 | 243 | ## Contributing 244 | 245 | If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement". 246 | 247 | If you found this useful, please give us a star! Thanks again! 248 | 249 |

(back to top)

250 | 251 | 252 | 253 | 254 |
255 | 256 | ## License 257 | 258 | Distributed under the MIT License. See `LICENSE.txt` for more information. 259 | 260 |

(back to top)

261 | 262 | 263 | 264 | 265 |
266 | 267 | ## Contact 268 | 269 | You can reach us, Arthur & Tyler, at [hello@openfoundry.ai](mailto:hello@openfoundry.ai). 270 | 271 | We’d love to hear from you! We’re excited to learn how we can make this more valuable for the community and welcome any and all feedback and suggestions. 272 | -------------------------------------------------------------------------------- /app/auth/callback/route.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from "@/utils/supabase/server"; 2 | import { NextResponse } from "next/server"; 3 | 4 | export async function GET(request: Request) { 5 | // The `/auth/callback` route is required for the server-side auth flow implemented 6 | // by the SSR package. It exchanges an auth code for the user's session. 7 | // https://supabase.com/docs/guides/auth/server-side/nextjs 8 | const requestUrl = new URL(request.url); 9 | const code = requestUrl.searchParams.get("code"); 10 | const origin = requestUrl.origin; 11 | 12 | if (code) { 13 | const supabase = createClient(); 14 | await supabase.auth.exchangeCodeForSession(code); 15 | } 16 | 17 | // URL to redirect to after sign up process completes 18 | return NextResponse.redirect(`${origin}/protected`); 19 | } 20 | -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 200 20% 98%; 8 | --btn-background: 200 10% 91%; 9 | --btn-background-hover: 200 10% 89%; 10 | --foreground: 200 50% 3%; 11 | } 12 | 13 | @media (prefers-color-scheme: dark) { 14 | :root { 15 | --background: 200 50% 3%; 16 | --btn-background: 200 10% 9%; 17 | --btn-background-hover: 200 10% 12%; 18 | --foreground: 200 20% 96%; 19 | } 20 | } 21 | } 22 | 23 | @layer base { 24 | * { 25 | @apply border-foreground/20; 26 | } 27 | } 28 | 29 | .animate-in { 30 | animation: animateIn 0.3s ease 0.15s both; 31 | } 32 | 33 | @keyframes animateIn { 34 | from { 35 | opacity: 0; 36 | transform: translateY(10px); 37 | } 38 | to { 39 | opacity: 1; 40 | transform: translateY(0); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { GeistSans } from "geist/font/sans"; 2 | import "./globals.css"; 3 | 4 | const defaultUrl = process.env.VERCEL_URL 5 | ? `https://${process.env.VERCEL_URL}` 6 | : "http://localhost:3000"; 7 | 8 | export const metadata = { 9 | metadataBase: new URL(defaultUrl), 10 | title: "Model Manager by Open Foundry", 11 | description: "", 12 | }; 13 | 14 | export default function RootLayout({ 15 | children, 16 | }: { 17 | children: React.ReactNode; 18 | }) { 19 | return ( 20 | 21 | 22 |
23 | {children} 24 |
25 | 26 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /app/login/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { headers } from "next/headers"; 3 | import { createClient } from "@/utils/supabase/server"; 4 | import { redirect } from "next/navigation"; 5 | import { SubmitButton } from "./submit-button"; 6 | 7 | export default function Login({ 8 | searchParams, 9 | }: { 10 | searchParams: { message: string }; 11 | }) { 12 | const signIn = async (formData: FormData) => { 13 | "use server"; 14 | 15 | const email = formData.get("email") as string; 16 | const password = formData.get("password") as string; 17 | const supabase = createClient(); 18 | 19 | const { error } = await supabase.auth.signInWithPassword({ 20 | email, 21 | password, 22 | }); 23 | 24 | if (error) { 25 | return redirect("/login?message=Could not authenticate user"); 26 | } 27 | 28 | return redirect("/protected"); 29 | }; 30 | 31 | const signUp = async (formData: FormData) => { 32 | "use server"; 33 | 34 | const origin = headers().get("origin"); 35 | const email = formData.get("email") as string; 36 | const password = formData.get("password") as string; 37 | const supabase = createClient(); 38 | 39 | const { error } = await supabase.auth.signUp({ 40 | email, 41 | password, 42 | options: { 43 | emailRedirectTo: `${origin}/auth/callback`, 44 | }, 45 | }); 46 | 47 | if (error) { 48 | return redirect("/login?message=Could not authenticate user"); 49 | } 50 | 51 | return redirect("/login?message=Check email to continue sign in process"); 52 | }; 53 | 54 | return ( 55 |
56 | 60 | 72 | 73 | {" "} 74 | Back 75 | 76 | 77 |
78 | 81 | 87 | 90 | 97 | 102 | Sign In 103 | 104 | 109 | Sign Up 110 | 111 | {searchParams?.message && ( 112 |

113 | {searchParams.message} 114 |

115 | )} 116 |
117 |
118 | ); 119 | } 120 | -------------------------------------------------------------------------------- /app/login/submit-button.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useFormStatus } from "react-dom"; 4 | import { type ComponentProps } from "react"; 5 | 6 | type Props = ComponentProps<"button"> & { 7 | pendingText?: string; 8 | }; 9 | 10 | export function SubmitButton({ children, pendingText, ...props }: Props) { 11 | const { pending, action } = useFormStatus(); 12 | 13 | const isPending = pending && action === props.formAction; 14 | 15 | return ( 16 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | import AuthButton from "../components/AuthButton"; 2 | import { createClient } from "@/utils/supabase/server"; 3 | 4 | export default async function Index() { 5 | const canInitSupabaseClient = () => { 6 | // This function is just for the interactive tutorial. 7 | // Feel free to remove it once you have Supabase connected. 8 | try { 9 | createClient(); 10 | return true; 11 | } catch (e) { 12 | return false; 13 | } 14 | }; 15 | 16 | const isSupabaseConnected = canInitSupabaseClient(); 17 | 18 | return ( 19 |
20 | 26 | 27 | 40 |
41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /app/protected/page.tsx: -------------------------------------------------------------------------------- 1 | import AuthButton from "@/components/AuthButton"; 2 | import { createClient } from "@/utils/supabase/server"; 3 | import Header from "@/components/Header"; 4 | import { redirect } from "next/navigation"; 5 | 6 | export default async function ProtectedPage() { 7 | const supabase = createClient(); 8 | 9 | const { 10 | data: { user }, 11 | } = await supabase.auth.getUser(); 12 | 13 | if (!user) { 14 | return redirect("/login"); 15 | } 16 | 17 | return ( 18 |
19 |
20 |
21 | This is a protected page that you can only see as an authenticated 22 | user 23 |
24 | 30 |
31 | 32 |
33 |
34 |
35 | 36 | 49 |
50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /components/AuthButton.tsx: -------------------------------------------------------------------------------- 1 | import { createClient } from "@/utils/supabase/server"; 2 | import Link from "next/link"; 3 | import { redirect } from "next/navigation"; 4 | 5 | export default async function AuthButton() { 6 | const supabase = createClient(); 7 | 8 | const { 9 | data: { user }, 10 | } = await supabase.auth.getUser(); 11 | 12 | const signOut = async () => { 13 | "use server"; 14 | 15 | const supabase = createClient(); 16 | await supabase.auth.signOut(); 17 | return redirect("/login"); 18 | }; 19 | 20 | return user ? ( 21 |
22 | Hey, {user.email}! 23 |
24 | 27 |
28 |
29 | ) : ( 30 | 34 | Login 35 | 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /components/Header.tsx: -------------------------------------------------------------------------------- 1 | export default function Header() { 2 | return ( 3 |
4 |
5 |
6 |
7 | ); 8 | } 9 | -------------------------------------------------------------------------------- /configs/huggingface-tc-bert-base-cased-202403291810.yaml: -------------------------------------------------------------------------------- 1 | deployment: !Deployment 2 | destination: aws 3 | endpoint_name: huggingface-tc-bert-base-cased-202403291810 4 | instance_count: 1 5 | instance_type: ml.m5.xlarge 6 | num_gpus: 1 7 | quantization: null 8 | models: 9 | - !Model 10 | location: null 11 | predict: null 12 | id: huggingface-tc-bert-base-cased 13 | version: 2.* 14 | source: sagemaker 15 | task: tc 16 | -------------------------------------------------------------------------------- /example_configs/bert-eqa.yaml: -------------------------------------------------------------------------------- 1 | deployment: !Deployment 2 | destination: aws 3 | # Endpoint name matches model_id for querying atm. 4 | endpoint_name: huggingface-eqa-bert-base-cased 5 | instance_count: 1 6 | instance_type: ml.m5.xlarge 7 | 8 | models: 9 | - !Model 10 | # Base model id that was finetuned 11 | id: huggingface-eqa-bert-base-cased 12 | source: custom 13 | 14 | # local or S3 path 15 | location: ./models/model.tar.gz -------------------------------------------------------------------------------- /example_configs/bert-tc.yaml: -------------------------------------------------------------------------------- 1 | # Minimum deployment config 2 | deployment: !Deployment 3 | destination: aws 4 | instance_type: ml.m5.xlarge 5 | 6 | models: 7 | - !Model 8 | id: huggingface-tc-bert-large-cased 9 | source: sagemaker -------------------------------------------------------------------------------- /example_configs/llama7b.yaml: -------------------------------------------------------------------------------- 1 | deployment: !Deployment 2 | destination: aws 3 | endpoint_name: test-llama2-7b 4 | instance_count: 1 5 | instance_type: ml.g5.12xlarge 6 | num_gpus: 4 7 | # quantization: bitsandbytes 8 | 9 | models: 10 | - !Model 11 | id: meta-llama/Meta-Llama-3-8B-Instruct 12 | source: huggingface 13 | predict: 14 | temperature: 0.9 15 | top_p: 0.9 16 | top_k: 20 17 | max_new_tokens: 250 -------------------------------------------------------------------------------- /example_configs/train-bert.yaml: -------------------------------------------------------------------------------- 1 | training: !Training 2 | destination: aws 3 | instance_type: ml.p3.2xlarge 4 | instance_count: 1 5 | training_input_path: s3://jumpstart-cache-prod-us-east-1/training-datasets/tc/data.csv 6 | hyperparameters: !Hyperparameters 7 | epochs: 1 8 | per_device_train_batch_size: 32 9 | learning_rate: 0.01 10 | 11 | models: 12 | - !Model 13 | id: tensorflow-tc-bert-en-uncased-L-12-H-768-A-12-2 14 | version: 1.0.0 15 | source: sagemaker -------------------------------------------------------------------------------- /middleware.ts: -------------------------------------------------------------------------------- 1 | import { type NextRequest } from "next/server"; 2 | import { updateSession } from "@/utils/supabase/middleware"; 3 | 4 | export async function middleware(request: NextRequest) { 5 | return await updateSession(request); 6 | } 7 | 8 | export const config = { 9 | matcher: [ 10 | /* 11 | * Match all request paths except: 12 | * - _next/static (static files) 13 | * - _next/image (image optimization files) 14 | * - favicon.ico (favicon file) 15 | * - images - .svg, .png, .jpg, .jpeg, .gif, .webp 16 | * Feel free to modify this pattern to include more paths. 17 | */ 18 | "/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)", 19 | ], 20 | }; 21 | -------------------------------------------------------------------------------- /model_manager.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import logging 3 | import traceback 4 | import yaml 5 | logging.getLogger("sagemaker.config").setLevel(logging.WARNING) 6 | logging.getLogger("botocore.credentials").setLevel(logging.WARNING) 7 | import os 8 | from src.sagemaker.create_model import deploy_huggingface_model, deploy_model 9 | from src.sagemaker.fine_tune_model import fine_tune_model 10 | from src.schemas.deployment import Deployment 11 | from src.schemas.model import Model 12 | 13 | 14 | if __name__ == '__main__': 15 | # Run setup if these files/directories don't already exist 16 | if (not os.path.exists(os.path.expanduser('~/.aws')) or not os.path.exists('.env')): 17 | os.system("bash setup.sh") 18 | 19 | parser = argparse.ArgumentParser( 20 | description="Create, deploy, query against models.", 21 | epilog="As an alternative to the commandline, params can be placed in a file, one per line, and specified on the commandline like '%(prog)s @params.conf'.", 22 | fromfile_prefix_chars='@') 23 | parser.add_argument( 24 | "--hf", 25 | help="Deploy a Hugging Face Model.", 26 | type=str 27 | ) 28 | parser.add_argument( 29 | "--instance", 30 | help="EC2 instance type to deploy to.", 31 | type=str 32 | ) 33 | parser.add_argument( 34 | "--deploy", 35 | help="path to YAML deployment configuration file", 36 | type=str 37 | ) 38 | parser.add_argument( 39 | "--train", 40 | help="path to YAML training configuration file", 41 | type=str 42 | ) 43 | parser.add_argument( 44 | "-v", 45 | "--verbose", 46 | help="increase output verbosity", 47 | action="store_true") 48 | args = parser.parse_args() 49 | 50 | # Setup logging 51 | if args.verbose: 52 | loglevel = logging.DEBUG 53 | else: 54 | loglevel = logging.INFO 55 | 56 | if args.hf is not None: 57 | instance_type = args.instance or "ml.m5.xlarge" 58 | predictor = deploy_huggingface_model(args.hf, instance_type) 59 | quit() 60 | 61 | if args.deploy is not None: 62 | try: 63 | deployment = None 64 | model = None 65 | with open(args.deploy) as config: 66 | configuration = yaml.safe_load(config) 67 | deployment = configuration['deployment'] 68 | 69 | # TODO: Support multi-model endpoints 70 | model = configuration['models'][0] 71 | deploy_model(deployment, model) 72 | except: 73 | traceback.print_exc() 74 | print("File not found") 75 | 76 | quit() 77 | 78 | if args.train is not None: 79 | try: 80 | train = None 81 | model = None 82 | with open(args.train) as config: 83 | configuration = yaml.safe_load(config) 84 | training = configuration['training'] 85 | model = configuration['models'][0] 86 | fine_tune_model(training, model) 87 | except: 88 | traceback.print_exc() 89 | print("File not found") 90 | 91 | quit() 92 | 93 | from src.main import main 94 | main(args, loglevel) 95 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | module.exports = nextConfig; 5 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "model_manager", 3 | "lockfileVersion": 3, 4 | "requires": true, 5 | "packages": { 6 | "": { 7 | "dependencies": { 8 | "@supabase/ssr": "latest", 9 | "@supabase/supabase-js": "latest", 10 | "autoprefixer": "10.4.17", 11 | "geist": "^1.2.1", 12 | "next": "latest", 13 | "postcss": "8.4.33", 14 | "react": "18.2.0", 15 | "react-dom": "18.2.0", 16 | "tailwindcss": "3.4.1", 17 | "typescript": "5.3.3" 18 | }, 19 | "devDependencies": { 20 | "@types/node": "20.11.5", 21 | "@types/react": "18.2.48", 22 | "@types/react-dom": "18.2.18", 23 | "encoding": "^0.1.13" 24 | } 25 | }, 26 | "node_modules/@alloc/quick-lru": { 27 | "version": "5.2.0", 28 | "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", 29 | "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", 30 | "engines": { 31 | "node": ">=10" 32 | }, 33 | "funding": { 34 | "url": "https://github.com/sponsors/sindresorhus" 35 | } 36 | }, 37 | "node_modules/@isaacs/cliui": { 38 | "version": "8.0.2", 39 | "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", 40 | "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", 41 | "dependencies": { 42 | "string-width": "^5.1.2", 43 | "string-width-cjs": "npm:string-width@^4.2.0", 44 | "strip-ansi": "^7.0.1", 45 | "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", 46 | "wrap-ansi": "^8.1.0", 47 | "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" 48 | }, 49 | "engines": { 50 | "node": ">=12" 51 | } 52 | }, 53 | "node_modules/@jridgewell/gen-mapping": { 54 | "version": "0.3.5", 55 | "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", 56 | "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", 57 | "dependencies": { 58 | "@jridgewell/set-array": "^1.2.1", 59 | "@jridgewell/sourcemap-codec": "^1.4.10", 60 | "@jridgewell/trace-mapping": "^0.3.24" 61 | }, 62 | "engines": { 63 | "node": ">=6.0.0" 64 | } 65 | }, 66 | "node_modules/@jridgewell/resolve-uri": { 67 | "version": "3.1.2", 68 | "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", 69 | "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", 70 | "engines": { 71 | "node": ">=6.0.0" 72 | } 73 | }, 74 | "node_modules/@jridgewell/set-array": { 75 | "version": "1.2.1", 76 | "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", 77 | "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", 78 | "engines": { 79 | "node": ">=6.0.0" 80 | } 81 | }, 82 | "node_modules/@jridgewell/sourcemap-codec": { 83 | "version": "1.4.15", 84 | "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", 85 | "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" 86 | }, 87 | "node_modules/@jridgewell/trace-mapping": { 88 | "version": "0.3.25", 89 | "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", 90 | "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", 91 | "dependencies": { 92 | "@jridgewell/resolve-uri": "^3.1.0", 93 | "@jridgewell/sourcemap-codec": "^1.4.14" 94 | } 95 | }, 96 | "node_modules/@next/env": { 97 | "version": "14.2.3", 98 | "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.3.tgz", 99 | "integrity": "sha512-W7fd7IbkfmeeY2gXrzJYDx8D2lWKbVoTIj1o1ScPHNzvp30s1AuoEFSdr39bC5sjxJaxTtq3OTCZboNp0lNWHA==" 100 | }, 101 | "node_modules/@next/swc-darwin-arm64": { 102 | "version": "14.2.3", 103 | "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.3.tgz", 104 | "integrity": "sha512-3pEYo/RaGqPP0YzwnlmPN2puaF2WMLM3apt5jLW2fFdXD9+pqcoTzRk+iZsf8ta7+quAe4Q6Ms0nR0SFGFdS1A==", 105 | "cpu": [ 106 | "arm64" 107 | ], 108 | "optional": true, 109 | "os": [ 110 | "darwin" 111 | ], 112 | "engines": { 113 | "node": ">= 10" 114 | } 115 | }, 116 | "node_modules/@next/swc-darwin-x64": { 117 | "version": "14.2.3", 118 | "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.3.tgz", 119 | "integrity": "sha512-6adp7waE6P1TYFSXpY366xwsOnEXM+y1kgRpjSRVI2CBDOcbRjsJ67Z6EgKIqWIue52d2q/Mx8g9MszARj8IEA==", 120 | "cpu": [ 121 | "x64" 122 | ], 123 | "optional": true, 124 | "os": [ 125 | "darwin" 126 | ], 127 | "engines": { 128 | "node": ">= 10" 129 | } 130 | }, 131 | "node_modules/@next/swc-linux-arm64-gnu": { 132 | "version": "14.2.3", 133 | "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.3.tgz", 134 | "integrity": "sha512-cuzCE/1G0ZSnTAHJPUT1rPgQx1w5tzSX7POXSLaS7w2nIUJUD+e25QoXD/hMfxbsT9rslEXugWypJMILBj/QsA==", 135 | "cpu": [ 136 | "arm64" 137 | ], 138 | "optional": true, 139 | "os": [ 140 | "linux" 141 | ], 142 | "engines": { 143 | "node": ">= 10" 144 | } 145 | }, 146 | "node_modules/@next/swc-linux-arm64-musl": { 147 | "version": "14.2.3", 148 | "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.3.tgz", 149 | "integrity": "sha512-0D4/oMM2Y9Ta3nGuCcQN8jjJjmDPYpHX9OJzqk42NZGJocU2MqhBq5tWkJrUQOQY9N+In9xOdymzapM09GeiZw==", 150 | "cpu": [ 151 | "arm64" 152 | ], 153 | "optional": true, 154 | "os": [ 155 | "linux" 156 | ], 157 | "engines": { 158 | "node": ">= 10" 159 | } 160 | }, 161 | "node_modules/@next/swc-linux-x64-gnu": { 162 | "version": "14.2.3", 163 | "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.3.tgz", 164 | "integrity": "sha512-ENPiNnBNDInBLyUU5ii8PMQh+4XLr4pG51tOp6aJ9xqFQ2iRI6IH0Ds2yJkAzNV1CfyagcyzPfROMViS2wOZ9w==", 165 | "cpu": [ 166 | "x64" 167 | ], 168 | "optional": true, 169 | "os": [ 170 | "linux" 171 | ], 172 | "engines": { 173 | "node": ">= 10" 174 | } 175 | }, 176 | "node_modules/@next/swc-linux-x64-musl": { 177 | "version": "14.2.3", 178 | "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.3.tgz", 179 | "integrity": "sha512-BTAbq0LnCbF5MtoM7I/9UeUu/8ZBY0i8SFjUMCbPDOLv+un67e2JgyN4pmgfXBwy/I+RHu8q+k+MCkDN6P9ViQ==", 180 | "cpu": [ 181 | "x64" 182 | ], 183 | "optional": true, 184 | "os": [ 185 | "linux" 186 | ], 187 | "engines": { 188 | "node": ">= 10" 189 | } 190 | }, 191 | "node_modules/@next/swc-win32-arm64-msvc": { 192 | "version": "14.2.3", 193 | "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.3.tgz", 194 | "integrity": "sha512-AEHIw/dhAMLNFJFJIJIyOFDzrzI5bAjI9J26gbO5xhAKHYTZ9Or04BesFPXiAYXDNdrwTP2dQceYA4dL1geu8A==", 195 | "cpu": [ 196 | "arm64" 197 | ], 198 | "optional": true, 199 | "os": [ 200 | "win32" 201 | ], 202 | "engines": { 203 | "node": ">= 10" 204 | } 205 | }, 206 | "node_modules/@next/swc-win32-ia32-msvc": { 207 | "version": "14.2.3", 208 | "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.3.tgz", 209 | "integrity": "sha512-vga40n1q6aYb0CLrM+eEmisfKCR45ixQYXuBXxOOmmoV8sYST9k7E3US32FsY+CkkF7NtzdcebiFT4CHuMSyZw==", 210 | "cpu": [ 211 | "ia32" 212 | ], 213 | "optional": true, 214 | "os": [ 215 | "win32" 216 | ], 217 | "engines": { 218 | "node": ">= 10" 219 | } 220 | }, 221 | "node_modules/@next/swc-win32-x64-msvc": { 222 | "version": "14.2.3", 223 | "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.3.tgz", 224 | "integrity": "sha512-Q1/zm43RWynxrO7lW4ehciQVj+5ePBhOK+/K2P7pLFX3JaJ/IZVC69SHidrmZSOkqz7ECIOhhy7XhAFG4JYyHA==", 225 | "cpu": [ 226 | "x64" 227 | ], 228 | "optional": true, 229 | "os": [ 230 | "win32" 231 | ], 232 | "engines": { 233 | "node": ">= 10" 234 | } 235 | }, 236 | "node_modules/@nodelib/fs.scandir": { 237 | "version": "2.1.5", 238 | "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", 239 | "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", 240 | "dependencies": { 241 | "@nodelib/fs.stat": "2.0.5", 242 | "run-parallel": "^1.1.9" 243 | }, 244 | "engines": { 245 | "node": ">= 8" 246 | } 247 | }, 248 | "node_modules/@nodelib/fs.stat": { 249 | "version": "2.0.5", 250 | "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", 251 | "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", 252 | "engines": { 253 | "node": ">= 8" 254 | } 255 | }, 256 | "node_modules/@nodelib/fs.walk": { 257 | "version": "1.2.8", 258 | "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", 259 | "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", 260 | "dependencies": { 261 | "@nodelib/fs.scandir": "2.1.5", 262 | "fastq": "^1.6.0" 263 | }, 264 | "engines": { 265 | "node": ">= 8" 266 | } 267 | }, 268 | "node_modules/@pkgjs/parseargs": { 269 | "version": "0.11.0", 270 | "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", 271 | "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", 272 | "optional": true, 273 | "engines": { 274 | "node": ">=14" 275 | } 276 | }, 277 | "node_modules/@supabase/auth-js": { 278 | "version": "2.64.2", 279 | "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.64.2.tgz", 280 | "integrity": "sha512-s+lkHEdGiczDrzXJ1YWt2y3bxRi+qIUnXcgkpLSrId7yjBeaXBFygNjTaoZLG02KNcYwbuZ9qkEIqmj2hF7svw==", 281 | "dependencies": { 282 | "@supabase/node-fetch": "^2.6.14" 283 | } 284 | }, 285 | "node_modules/@supabase/functions-js": { 286 | "version": "2.3.1", 287 | "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.3.1.tgz", 288 | "integrity": "sha512-QyzNle/rVzlOi4BbVqxLSH828VdGY1RElqGFAj+XeVypj6+PVtMlD21G8SDnsPQDtlqqTtoGRgdMlQZih5hTuw==", 289 | "dependencies": { 290 | "@supabase/node-fetch": "^2.6.14" 291 | } 292 | }, 293 | "node_modules/@supabase/node-fetch": { 294 | "version": "2.6.15", 295 | "resolved": "https://registry.npmjs.org/@supabase/node-fetch/-/node-fetch-2.6.15.tgz", 296 | "integrity": "sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==", 297 | "dependencies": { 298 | "whatwg-url": "^5.0.0" 299 | }, 300 | "engines": { 301 | "node": "4.x || >=6.0.0" 302 | } 303 | }, 304 | "node_modules/@supabase/postgrest-js": { 305 | "version": "1.15.2", 306 | "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.15.2.tgz", 307 | "integrity": "sha512-9/7pUmXExvGuEK1yZhVYXPZnLEkDTwxgMQHXLrN5BwPZZm4iUCL1YEyep/Z2lIZah8d8M433mVAUEGsihUj5KQ==", 308 | "dependencies": { 309 | "@supabase/node-fetch": "^2.6.14" 310 | } 311 | }, 312 | "node_modules/@supabase/realtime-js": { 313 | "version": "2.9.5", 314 | "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.9.5.tgz", 315 | "integrity": "sha512-TEHlGwNGGmKPdeMtca1lFTYCedrhTAv3nZVoSjrKQ+wkMmaERuCe57zkC5KSWFzLYkb5FVHW8Hrr+PX1DDwplQ==", 316 | "dependencies": { 317 | "@supabase/node-fetch": "^2.6.14", 318 | "@types/phoenix": "^1.5.4", 319 | "@types/ws": "^8.5.10", 320 | "ws": "^8.14.2" 321 | } 322 | }, 323 | "node_modules/@supabase/ssr": { 324 | "version": "0.3.0", 325 | "resolved": "https://registry.npmjs.org/@supabase/ssr/-/ssr-0.3.0.tgz", 326 | "integrity": "sha512-lcVyQ7H6eumb2FB1Wa2N+jYWMfq6CFza3KapikT0fgttMQ+QvDgpNogx9jI8bZgKds+XFSMCojxFvFb+gwdbfA==", 327 | "dependencies": { 328 | "cookie": "^0.5.0", 329 | "ramda": "^0.29.0" 330 | }, 331 | "peerDependencies": { 332 | "@supabase/supabase-js": "^2.33.1" 333 | } 334 | }, 335 | "node_modules/@supabase/storage-js": { 336 | "version": "2.5.5", 337 | "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.5.5.tgz", 338 | "integrity": "sha512-OpLoDRjFwClwc2cjTJZG8XviTiQH4Ik8sCiMK5v7et0MDu2QlXjCAW3ljxJB5+z/KazdMOTnySi+hysxWUPu3w==", 339 | "dependencies": { 340 | "@supabase/node-fetch": "^2.6.14" 341 | } 342 | }, 343 | "node_modules/@supabase/supabase-js": { 344 | "version": "2.43.1", 345 | "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.43.1.tgz", 346 | "integrity": "sha512-A+RV50mWNtyKo6M0u4G6AOqEifQD+MoOjZcpRkPMPpEAFgMsc2dt3kBlBlR/MgZizWQgUKhsvrwKk0efc8g6Ug==", 347 | "dependencies": { 348 | "@supabase/auth-js": "2.64.2", 349 | "@supabase/functions-js": "2.3.1", 350 | "@supabase/node-fetch": "2.6.15", 351 | "@supabase/postgrest-js": "1.15.2", 352 | "@supabase/realtime-js": "2.9.5", 353 | "@supabase/storage-js": "2.5.5" 354 | } 355 | }, 356 | "node_modules/@swc/counter": { 357 | "version": "0.1.3", 358 | "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", 359 | "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==" 360 | }, 361 | "node_modules/@swc/helpers": { 362 | "version": "0.5.5", 363 | "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.5.tgz", 364 | "integrity": "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==", 365 | "dependencies": { 366 | "@swc/counter": "^0.1.3", 367 | "tslib": "^2.4.0" 368 | } 369 | }, 370 | "node_modules/@types/node": { 371 | "version": "20.11.5", 372 | "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.5.tgz", 373 | "integrity": "sha512-g557vgQjUUfN76MZAN/dt1z3dzcUsimuysco0KeluHgrPdJXkP/XdAURgyO2W9fZWHRtRBiVKzKn8vyOAwlG+w==", 374 | "dependencies": { 375 | "undici-types": "~5.26.4" 376 | } 377 | }, 378 | "node_modules/@types/phoenix": { 379 | "version": "1.6.4", 380 | "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.4.tgz", 381 | "integrity": "sha512-B34A7uot1Cv0XtaHRYDATltAdKx0BvVKNgYNqE4WjtPUa4VQJM7kxeXcVKaH+KS+kCmZ+6w+QaUdcljiheiBJA==" 382 | }, 383 | "node_modules/@types/prop-types": { 384 | "version": "15.7.12", 385 | "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", 386 | "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==", 387 | "dev": true 388 | }, 389 | "node_modules/@types/react": { 390 | "version": "18.2.48", 391 | "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.48.tgz", 392 | "integrity": "sha512-qboRCl6Ie70DQQG9hhNREz81jqC1cs9EVNcjQ1AU+jH6NFfSAhVVbrrY/+nSF+Bsk4AOwm9Qa61InvMCyV+H3w==", 393 | "dev": true, 394 | "dependencies": { 395 | "@types/prop-types": "*", 396 | "@types/scheduler": "*", 397 | "csstype": "^3.0.2" 398 | } 399 | }, 400 | "node_modules/@types/react-dom": { 401 | "version": "18.2.18", 402 | "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.18.tgz", 403 | "integrity": "sha512-TJxDm6OfAX2KJWJdMEVTwWke5Sc/E/RlnPGvGfS0W7+6ocy2xhDVQVh/KvC2Uf7kACs+gDytdusDSdWfWkaNzw==", 404 | "dev": true, 405 | "dependencies": { 406 | "@types/react": "*" 407 | } 408 | }, 409 | "node_modules/@types/scheduler": { 410 | "version": "0.23.0", 411 | "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.23.0.tgz", 412 | "integrity": "sha512-YIoDCTH3Af6XM5VuwGG/QL/CJqga1Zm3NkU3HZ4ZHK2fRMPYP1VczsTUqtsf43PH/iJNVlPHAo2oWX7BSdB2Hw==", 413 | "dev": true 414 | }, 415 | "node_modules/@types/ws": { 416 | "version": "8.5.10", 417 | "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", 418 | "integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==", 419 | "dependencies": { 420 | "@types/node": "*" 421 | } 422 | }, 423 | "node_modules/ansi-regex": { 424 | "version": "6.0.1", 425 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", 426 | "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", 427 | "engines": { 428 | "node": ">=12" 429 | }, 430 | "funding": { 431 | "url": "https://github.com/chalk/ansi-regex?sponsor=1" 432 | } 433 | }, 434 | "node_modules/ansi-styles": { 435 | "version": "6.2.1", 436 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", 437 | "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", 438 | "engines": { 439 | "node": ">=12" 440 | }, 441 | "funding": { 442 | "url": "https://github.com/chalk/ansi-styles?sponsor=1" 443 | } 444 | }, 445 | "node_modules/any-promise": { 446 | "version": "1.3.0", 447 | "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", 448 | "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==" 449 | }, 450 | "node_modules/anymatch": { 451 | "version": "3.1.3", 452 | "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", 453 | "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", 454 | "dependencies": { 455 | "normalize-path": "^3.0.0", 456 | "picomatch": "^2.0.4" 457 | }, 458 | "engines": { 459 | "node": ">= 8" 460 | } 461 | }, 462 | "node_modules/arg": { 463 | "version": "5.0.2", 464 | "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", 465 | "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==" 466 | }, 467 | "node_modules/autoprefixer": { 468 | "version": "10.4.17", 469 | "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.17.tgz", 470 | "integrity": "sha512-/cpVNRLSfhOtcGflT13P2794gVSgmPgTR+erw5ifnMLZb0UnSlkK4tquLmkd3BhA+nLo5tX8Cu0upUsGKvKbmg==", 471 | "funding": [ 472 | { 473 | "type": "opencollective", 474 | "url": "https://opencollective.com/postcss/" 475 | }, 476 | { 477 | "type": "tidelift", 478 | "url": "https://tidelift.com/funding/github/npm/autoprefixer" 479 | }, 480 | { 481 | "type": "github", 482 | "url": "https://github.com/sponsors/ai" 483 | } 484 | ], 485 | "dependencies": { 486 | "browserslist": "^4.22.2", 487 | "caniuse-lite": "^1.0.30001578", 488 | "fraction.js": "^4.3.7", 489 | "normalize-range": "^0.1.2", 490 | "picocolors": "^1.0.0", 491 | "postcss-value-parser": "^4.2.0" 492 | }, 493 | "bin": { 494 | "autoprefixer": "bin/autoprefixer" 495 | }, 496 | "engines": { 497 | "node": "^10 || ^12 || >=14" 498 | }, 499 | "peerDependencies": { 500 | "postcss": "^8.1.0" 501 | } 502 | }, 503 | "node_modules/balanced-match": { 504 | "version": "1.0.2", 505 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 506 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" 507 | }, 508 | "node_modules/binary-extensions": { 509 | "version": "2.3.0", 510 | "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", 511 | "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", 512 | "engines": { 513 | "node": ">=8" 514 | }, 515 | "funding": { 516 | "url": "https://github.com/sponsors/sindresorhus" 517 | } 518 | }, 519 | "node_modules/brace-expansion": { 520 | "version": "2.0.1", 521 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", 522 | "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", 523 | "dependencies": { 524 | "balanced-match": "^1.0.0" 525 | } 526 | }, 527 | "node_modules/braces": { 528 | "version": "3.0.2", 529 | "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", 530 | "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", 531 | "dependencies": { 532 | "fill-range": "^7.0.1" 533 | }, 534 | "engines": { 535 | "node": ">=8" 536 | } 537 | }, 538 | "node_modules/browserslist": { 539 | "version": "4.23.0", 540 | "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", 541 | "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", 542 | "funding": [ 543 | { 544 | "type": "opencollective", 545 | "url": "https://opencollective.com/browserslist" 546 | }, 547 | { 548 | "type": "tidelift", 549 | "url": "https://tidelift.com/funding/github/npm/browserslist" 550 | }, 551 | { 552 | "type": "github", 553 | "url": "https://github.com/sponsors/ai" 554 | } 555 | ], 556 | "dependencies": { 557 | "caniuse-lite": "^1.0.30001587", 558 | "electron-to-chromium": "^1.4.668", 559 | "node-releases": "^2.0.14", 560 | "update-browserslist-db": "^1.0.13" 561 | }, 562 | "bin": { 563 | "browserslist": "cli.js" 564 | }, 565 | "engines": { 566 | "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" 567 | } 568 | }, 569 | "node_modules/busboy": { 570 | "version": "1.6.0", 571 | "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", 572 | "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", 573 | "dependencies": { 574 | "streamsearch": "^1.1.0" 575 | }, 576 | "engines": { 577 | "node": ">=10.16.0" 578 | } 579 | }, 580 | "node_modules/camelcase-css": { 581 | "version": "2.0.1", 582 | "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", 583 | "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", 584 | "engines": { 585 | "node": ">= 6" 586 | } 587 | }, 588 | "node_modules/caniuse-lite": { 589 | "version": "1.0.30001618", 590 | "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001618.tgz", 591 | "integrity": "sha512-p407+D1tIkDvsEAPS22lJxLQQaG8OTBEqo0KhzfABGk0TU4juBNDSfH0hyAp/HRyx+M8L17z/ltyhxh27FTfQg==", 592 | "funding": [ 593 | { 594 | "type": "opencollective", 595 | "url": "https://opencollective.com/browserslist" 596 | }, 597 | { 598 | "type": "tidelift", 599 | "url": "https://tidelift.com/funding/github/npm/caniuse-lite" 600 | }, 601 | { 602 | "type": "github", 603 | "url": "https://github.com/sponsors/ai" 604 | } 605 | ] 606 | }, 607 | "node_modules/chokidar": { 608 | "version": "3.6.0", 609 | "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", 610 | "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", 611 | "dependencies": { 612 | "anymatch": "~3.1.2", 613 | "braces": "~3.0.2", 614 | "glob-parent": "~5.1.2", 615 | "is-binary-path": "~2.1.0", 616 | "is-glob": "~4.0.1", 617 | "normalize-path": "~3.0.0", 618 | "readdirp": "~3.6.0" 619 | }, 620 | "engines": { 621 | "node": ">= 8.10.0" 622 | }, 623 | "funding": { 624 | "url": "https://paulmillr.com/funding/" 625 | }, 626 | "optionalDependencies": { 627 | "fsevents": "~2.3.2" 628 | } 629 | }, 630 | "node_modules/chokidar/node_modules/glob-parent": { 631 | "version": "5.1.2", 632 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", 633 | "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", 634 | "dependencies": { 635 | "is-glob": "^4.0.1" 636 | }, 637 | "engines": { 638 | "node": ">= 6" 639 | } 640 | }, 641 | "node_modules/client-only": { 642 | "version": "0.0.1", 643 | "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", 644 | "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==" 645 | }, 646 | "node_modules/color-convert": { 647 | "version": "2.0.1", 648 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 649 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 650 | "dependencies": { 651 | "color-name": "~1.1.4" 652 | }, 653 | "engines": { 654 | "node": ">=7.0.0" 655 | } 656 | }, 657 | "node_modules/color-name": { 658 | "version": "1.1.4", 659 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 660 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" 661 | }, 662 | "node_modules/commander": { 663 | "version": "4.1.1", 664 | "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", 665 | "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", 666 | "engines": { 667 | "node": ">= 6" 668 | } 669 | }, 670 | "node_modules/cookie": { 671 | "version": "0.5.0", 672 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", 673 | "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", 674 | "engines": { 675 | "node": ">= 0.6" 676 | } 677 | }, 678 | "node_modules/cross-spawn": { 679 | "version": "7.0.3", 680 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", 681 | "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", 682 | "dependencies": { 683 | "path-key": "^3.1.0", 684 | "shebang-command": "^2.0.0", 685 | "which": "^2.0.1" 686 | }, 687 | "engines": { 688 | "node": ">= 8" 689 | } 690 | }, 691 | "node_modules/cssesc": { 692 | "version": "3.0.0", 693 | "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", 694 | "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", 695 | "bin": { 696 | "cssesc": "bin/cssesc" 697 | }, 698 | "engines": { 699 | "node": ">=4" 700 | } 701 | }, 702 | "node_modules/csstype": { 703 | "version": "3.1.3", 704 | "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", 705 | "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", 706 | "dev": true 707 | }, 708 | "node_modules/didyoumean": { 709 | "version": "1.2.2", 710 | "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", 711 | "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==" 712 | }, 713 | "node_modules/dlv": { 714 | "version": "1.1.3", 715 | "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", 716 | "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" 717 | }, 718 | "node_modules/eastasianwidth": { 719 | "version": "0.2.0", 720 | "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", 721 | "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" 722 | }, 723 | "node_modules/electron-to-chromium": { 724 | "version": "1.4.768", 725 | "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.768.tgz", 726 | "integrity": "sha512-z2U3QcvNuxdkk33YV7R1bVMNq7fL23vq3WfO5BHcqrm4TnDGReouBfYKLEFh5umoK1XACjEwp8mmnhXk2EJigw==" 727 | }, 728 | "node_modules/emoji-regex": { 729 | "version": "9.2.2", 730 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", 731 | "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" 732 | }, 733 | "node_modules/encoding": { 734 | "version": "0.1.13", 735 | "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", 736 | "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", 737 | "dev": true, 738 | "dependencies": { 739 | "iconv-lite": "^0.6.2" 740 | } 741 | }, 742 | "node_modules/escalade": { 743 | "version": "3.1.2", 744 | "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", 745 | "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", 746 | "engines": { 747 | "node": ">=6" 748 | } 749 | }, 750 | "node_modules/fast-glob": { 751 | "version": "3.3.2", 752 | "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", 753 | "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", 754 | "dependencies": { 755 | "@nodelib/fs.stat": "^2.0.2", 756 | "@nodelib/fs.walk": "^1.2.3", 757 | "glob-parent": "^5.1.2", 758 | "merge2": "^1.3.0", 759 | "micromatch": "^4.0.4" 760 | }, 761 | "engines": { 762 | "node": ">=8.6.0" 763 | } 764 | }, 765 | "node_modules/fast-glob/node_modules/glob-parent": { 766 | "version": "5.1.2", 767 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", 768 | "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", 769 | "dependencies": { 770 | "is-glob": "^4.0.1" 771 | }, 772 | "engines": { 773 | "node": ">= 6" 774 | } 775 | }, 776 | "node_modules/fastq": { 777 | "version": "1.17.1", 778 | "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", 779 | "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", 780 | "dependencies": { 781 | "reusify": "^1.0.4" 782 | } 783 | }, 784 | "node_modules/fill-range": { 785 | "version": "7.0.1", 786 | "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", 787 | "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", 788 | "dependencies": { 789 | "to-regex-range": "^5.0.1" 790 | }, 791 | "engines": { 792 | "node": ">=8" 793 | } 794 | }, 795 | "node_modules/foreground-child": { 796 | "version": "3.1.1", 797 | "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", 798 | "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", 799 | "dependencies": { 800 | "cross-spawn": "^7.0.0", 801 | "signal-exit": "^4.0.1" 802 | }, 803 | "engines": { 804 | "node": ">=14" 805 | }, 806 | "funding": { 807 | "url": "https://github.com/sponsors/isaacs" 808 | } 809 | }, 810 | "node_modules/fraction.js": { 811 | "version": "4.3.7", 812 | "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", 813 | "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", 814 | "engines": { 815 | "node": "*" 816 | }, 817 | "funding": { 818 | "type": "patreon", 819 | "url": "https://github.com/sponsors/rawify" 820 | } 821 | }, 822 | "node_modules/fsevents": { 823 | "version": "2.3.3", 824 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", 825 | "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", 826 | "hasInstallScript": true, 827 | "optional": true, 828 | "os": [ 829 | "darwin" 830 | ], 831 | "engines": { 832 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 833 | } 834 | }, 835 | "node_modules/function-bind": { 836 | "version": "1.1.2", 837 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", 838 | "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", 839 | "funding": { 840 | "url": "https://github.com/sponsors/ljharb" 841 | } 842 | }, 843 | "node_modules/geist": { 844 | "version": "1.3.0", 845 | "resolved": "https://registry.npmjs.org/geist/-/geist-1.3.0.tgz", 846 | "integrity": "sha512-IoGBfcqVEYB4bEwsfHd35jF4+X9LHRPYZymHL4YOltHSs9LJa24DYs1Z7rEMQ/lsEvaAIc61Y9aUxgcJaQ8lrg==", 847 | "peerDependencies": { 848 | "next": ">=13.2.0 <15.0.0-0" 849 | } 850 | }, 851 | "node_modules/glob": { 852 | "version": "10.3.15", 853 | "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.15.tgz", 854 | "integrity": "sha512-0c6RlJt1TICLyvJYIApxb8GsXoai0KUP7AxKKAtsYXdgJR1mGEUa7DgwShbdk1nly0PYoZj01xd4hzbq3fsjpw==", 855 | "dependencies": { 856 | "foreground-child": "^3.1.0", 857 | "jackspeak": "^2.3.6", 858 | "minimatch": "^9.0.1", 859 | "minipass": "^7.0.4", 860 | "path-scurry": "^1.11.0" 861 | }, 862 | "bin": { 863 | "glob": "dist/esm/bin.mjs" 864 | }, 865 | "engines": { 866 | "node": ">=16 || 14 >=14.18" 867 | }, 868 | "funding": { 869 | "url": "https://github.com/sponsors/isaacs" 870 | } 871 | }, 872 | "node_modules/glob-parent": { 873 | "version": "6.0.2", 874 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", 875 | "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", 876 | "dependencies": { 877 | "is-glob": "^4.0.3" 878 | }, 879 | "engines": { 880 | "node": ">=10.13.0" 881 | } 882 | }, 883 | "node_modules/graceful-fs": { 884 | "version": "4.2.11", 885 | "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", 886 | "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" 887 | }, 888 | "node_modules/hasown": { 889 | "version": "2.0.2", 890 | "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", 891 | "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", 892 | "dependencies": { 893 | "function-bind": "^1.1.2" 894 | }, 895 | "engines": { 896 | "node": ">= 0.4" 897 | } 898 | }, 899 | "node_modules/iconv-lite": { 900 | "version": "0.6.3", 901 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", 902 | "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", 903 | "dev": true, 904 | "dependencies": { 905 | "safer-buffer": ">= 2.1.2 < 3.0.0" 906 | }, 907 | "engines": { 908 | "node": ">=0.10.0" 909 | } 910 | }, 911 | "node_modules/is-binary-path": { 912 | "version": "2.1.0", 913 | "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", 914 | "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", 915 | "dependencies": { 916 | "binary-extensions": "^2.0.0" 917 | }, 918 | "engines": { 919 | "node": ">=8" 920 | } 921 | }, 922 | "node_modules/is-core-module": { 923 | "version": "2.13.1", 924 | "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", 925 | "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", 926 | "dependencies": { 927 | "hasown": "^2.0.0" 928 | }, 929 | "funding": { 930 | "url": "https://github.com/sponsors/ljharb" 931 | } 932 | }, 933 | "node_modules/is-extglob": { 934 | "version": "2.1.1", 935 | "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", 936 | "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", 937 | "engines": { 938 | "node": ">=0.10.0" 939 | } 940 | }, 941 | "node_modules/is-fullwidth-code-point": { 942 | "version": "3.0.0", 943 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", 944 | "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", 945 | "engines": { 946 | "node": ">=8" 947 | } 948 | }, 949 | "node_modules/is-glob": { 950 | "version": "4.0.3", 951 | "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", 952 | "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", 953 | "dependencies": { 954 | "is-extglob": "^2.1.1" 955 | }, 956 | "engines": { 957 | "node": ">=0.10.0" 958 | } 959 | }, 960 | "node_modules/is-number": { 961 | "version": "7.0.0", 962 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", 963 | "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", 964 | "engines": { 965 | "node": ">=0.12.0" 966 | } 967 | }, 968 | "node_modules/isexe": { 969 | "version": "2.0.0", 970 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 971 | "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" 972 | }, 973 | "node_modules/jackspeak": { 974 | "version": "2.3.6", 975 | "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", 976 | "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", 977 | "dependencies": { 978 | "@isaacs/cliui": "^8.0.2" 979 | }, 980 | "engines": { 981 | "node": ">=14" 982 | }, 983 | "funding": { 984 | "url": "https://github.com/sponsors/isaacs" 985 | }, 986 | "optionalDependencies": { 987 | "@pkgjs/parseargs": "^0.11.0" 988 | } 989 | }, 990 | "node_modules/jiti": { 991 | "version": "1.21.0", 992 | "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz", 993 | "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==", 994 | "bin": { 995 | "jiti": "bin/jiti.js" 996 | } 997 | }, 998 | "node_modules/js-tokens": { 999 | "version": "4.0.0", 1000 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", 1001 | "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" 1002 | }, 1003 | "node_modules/lilconfig": { 1004 | "version": "2.1.0", 1005 | "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", 1006 | "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", 1007 | "engines": { 1008 | "node": ">=10" 1009 | } 1010 | }, 1011 | "node_modules/lines-and-columns": { 1012 | "version": "1.2.4", 1013 | "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", 1014 | "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" 1015 | }, 1016 | "node_modules/loose-envify": { 1017 | "version": "1.4.0", 1018 | "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", 1019 | "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", 1020 | "dependencies": { 1021 | "js-tokens": "^3.0.0 || ^4.0.0" 1022 | }, 1023 | "bin": { 1024 | "loose-envify": "cli.js" 1025 | } 1026 | }, 1027 | "node_modules/lru-cache": { 1028 | "version": "10.2.2", 1029 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", 1030 | "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", 1031 | "engines": { 1032 | "node": "14 || >=16.14" 1033 | } 1034 | }, 1035 | "node_modules/merge2": { 1036 | "version": "1.4.1", 1037 | "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", 1038 | "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", 1039 | "engines": { 1040 | "node": ">= 8" 1041 | } 1042 | }, 1043 | "node_modules/micromatch": { 1044 | "version": "4.0.5", 1045 | "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", 1046 | "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", 1047 | "dependencies": { 1048 | "braces": "^3.0.2", 1049 | "picomatch": "^2.3.1" 1050 | }, 1051 | "engines": { 1052 | "node": ">=8.6" 1053 | } 1054 | }, 1055 | "node_modules/minimatch": { 1056 | "version": "9.0.4", 1057 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", 1058 | "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", 1059 | "dependencies": { 1060 | "brace-expansion": "^2.0.1" 1061 | }, 1062 | "engines": { 1063 | "node": ">=16 || 14 >=14.17" 1064 | }, 1065 | "funding": { 1066 | "url": "https://github.com/sponsors/isaacs" 1067 | } 1068 | }, 1069 | "node_modules/minipass": { 1070 | "version": "7.1.1", 1071 | "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.1.tgz", 1072 | "integrity": "sha512-UZ7eQ+h8ywIRAW1hIEl2AqdwzJucU/Kp59+8kkZeSvafXhZjul247BvIJjEVFVeON6d7lM46XX1HXCduKAS8VA==", 1073 | "engines": { 1074 | "node": ">=16 || 14 >=14.17" 1075 | } 1076 | }, 1077 | "node_modules/mz": { 1078 | "version": "2.7.0", 1079 | "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", 1080 | "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", 1081 | "dependencies": { 1082 | "any-promise": "^1.0.0", 1083 | "object-assign": "^4.0.1", 1084 | "thenify-all": "^1.0.0" 1085 | } 1086 | }, 1087 | "node_modules/nanoid": { 1088 | "version": "3.3.7", 1089 | "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", 1090 | "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", 1091 | "funding": [ 1092 | { 1093 | "type": "github", 1094 | "url": "https://github.com/sponsors/ai" 1095 | } 1096 | ], 1097 | "bin": { 1098 | "nanoid": "bin/nanoid.cjs" 1099 | }, 1100 | "engines": { 1101 | "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" 1102 | } 1103 | }, 1104 | "node_modules/next": { 1105 | "version": "14.2.3", 1106 | "resolved": "https://registry.npmjs.org/next/-/next-14.2.3.tgz", 1107 | "integrity": "sha512-dowFkFTR8v79NPJO4QsBUtxv0g9BrS/phluVpMAt2ku7H+cbcBJlopXjkWlwxrk/xGqMemr7JkGPGemPrLLX7A==", 1108 | "dependencies": { 1109 | "@next/env": "14.2.3", 1110 | "@swc/helpers": "0.5.5", 1111 | "busboy": "1.6.0", 1112 | "caniuse-lite": "^1.0.30001579", 1113 | "graceful-fs": "^4.2.11", 1114 | "postcss": "8.4.31", 1115 | "styled-jsx": "5.1.1" 1116 | }, 1117 | "bin": { 1118 | "next": "dist/bin/next" 1119 | }, 1120 | "engines": { 1121 | "node": ">=18.17.0" 1122 | }, 1123 | "optionalDependencies": { 1124 | "@next/swc-darwin-arm64": "14.2.3", 1125 | "@next/swc-darwin-x64": "14.2.3", 1126 | "@next/swc-linux-arm64-gnu": "14.2.3", 1127 | "@next/swc-linux-arm64-musl": "14.2.3", 1128 | "@next/swc-linux-x64-gnu": "14.2.3", 1129 | "@next/swc-linux-x64-musl": "14.2.3", 1130 | "@next/swc-win32-arm64-msvc": "14.2.3", 1131 | "@next/swc-win32-ia32-msvc": "14.2.3", 1132 | "@next/swc-win32-x64-msvc": "14.2.3" 1133 | }, 1134 | "peerDependencies": { 1135 | "@opentelemetry/api": "^1.1.0", 1136 | "@playwright/test": "^1.41.2", 1137 | "react": "^18.2.0", 1138 | "react-dom": "^18.2.0", 1139 | "sass": "^1.3.0" 1140 | }, 1141 | "peerDependenciesMeta": { 1142 | "@opentelemetry/api": { 1143 | "optional": true 1144 | }, 1145 | "@playwright/test": { 1146 | "optional": true 1147 | }, 1148 | "sass": { 1149 | "optional": true 1150 | } 1151 | } 1152 | }, 1153 | "node_modules/next/node_modules/postcss": { 1154 | "version": "8.4.31", 1155 | "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", 1156 | "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", 1157 | "funding": [ 1158 | { 1159 | "type": "opencollective", 1160 | "url": "https://opencollective.com/postcss/" 1161 | }, 1162 | { 1163 | "type": "tidelift", 1164 | "url": "https://tidelift.com/funding/github/npm/postcss" 1165 | }, 1166 | { 1167 | "type": "github", 1168 | "url": "https://github.com/sponsors/ai" 1169 | } 1170 | ], 1171 | "dependencies": { 1172 | "nanoid": "^3.3.6", 1173 | "picocolors": "^1.0.0", 1174 | "source-map-js": "^1.0.2" 1175 | }, 1176 | "engines": { 1177 | "node": "^10 || ^12 || >=14" 1178 | } 1179 | }, 1180 | "node_modules/node-releases": { 1181 | "version": "2.0.14", 1182 | "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", 1183 | "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==" 1184 | }, 1185 | "node_modules/normalize-path": { 1186 | "version": "3.0.0", 1187 | "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", 1188 | "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", 1189 | "engines": { 1190 | "node": ">=0.10.0" 1191 | } 1192 | }, 1193 | "node_modules/normalize-range": { 1194 | "version": "0.1.2", 1195 | "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", 1196 | "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", 1197 | "engines": { 1198 | "node": ">=0.10.0" 1199 | } 1200 | }, 1201 | "node_modules/object-assign": { 1202 | "version": "4.1.1", 1203 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 1204 | "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", 1205 | "engines": { 1206 | "node": ">=0.10.0" 1207 | } 1208 | }, 1209 | "node_modules/object-hash": { 1210 | "version": "3.0.0", 1211 | "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", 1212 | "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", 1213 | "engines": { 1214 | "node": ">= 6" 1215 | } 1216 | }, 1217 | "node_modules/path-key": { 1218 | "version": "3.1.1", 1219 | "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", 1220 | "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", 1221 | "engines": { 1222 | "node": ">=8" 1223 | } 1224 | }, 1225 | "node_modules/path-parse": { 1226 | "version": "1.0.7", 1227 | "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", 1228 | "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" 1229 | }, 1230 | "node_modules/path-scurry": { 1231 | "version": "1.11.1", 1232 | "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", 1233 | "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", 1234 | "dependencies": { 1235 | "lru-cache": "^10.2.0", 1236 | "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" 1237 | }, 1238 | "engines": { 1239 | "node": ">=16 || 14 >=14.18" 1240 | }, 1241 | "funding": { 1242 | "url": "https://github.com/sponsors/isaacs" 1243 | } 1244 | }, 1245 | "node_modules/picocolors": { 1246 | "version": "1.0.1", 1247 | "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", 1248 | "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==" 1249 | }, 1250 | "node_modules/picomatch": { 1251 | "version": "2.3.1", 1252 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", 1253 | "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", 1254 | "engines": { 1255 | "node": ">=8.6" 1256 | }, 1257 | "funding": { 1258 | "url": "https://github.com/sponsors/jonschlinkert" 1259 | } 1260 | }, 1261 | "node_modules/pify": { 1262 | "version": "2.3.0", 1263 | "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", 1264 | "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", 1265 | "engines": { 1266 | "node": ">=0.10.0" 1267 | } 1268 | }, 1269 | "node_modules/pirates": { 1270 | "version": "4.0.6", 1271 | "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", 1272 | "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", 1273 | "engines": { 1274 | "node": ">= 6" 1275 | } 1276 | }, 1277 | "node_modules/postcss": { 1278 | "version": "8.4.33", 1279 | "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.33.tgz", 1280 | "integrity": "sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==", 1281 | "funding": [ 1282 | { 1283 | "type": "opencollective", 1284 | "url": "https://opencollective.com/postcss/" 1285 | }, 1286 | { 1287 | "type": "tidelift", 1288 | "url": "https://tidelift.com/funding/github/npm/postcss" 1289 | }, 1290 | { 1291 | "type": "github", 1292 | "url": "https://github.com/sponsors/ai" 1293 | } 1294 | ], 1295 | "dependencies": { 1296 | "nanoid": "^3.3.7", 1297 | "picocolors": "^1.0.0", 1298 | "source-map-js": "^1.0.2" 1299 | }, 1300 | "engines": { 1301 | "node": "^10 || ^12 || >=14" 1302 | } 1303 | }, 1304 | "node_modules/postcss-import": { 1305 | "version": "15.1.0", 1306 | "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", 1307 | "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", 1308 | "dependencies": { 1309 | "postcss-value-parser": "^4.0.0", 1310 | "read-cache": "^1.0.0", 1311 | "resolve": "^1.1.7" 1312 | }, 1313 | "engines": { 1314 | "node": ">=14.0.0" 1315 | }, 1316 | "peerDependencies": { 1317 | "postcss": "^8.0.0" 1318 | } 1319 | }, 1320 | "node_modules/postcss-js": { 1321 | "version": "4.0.1", 1322 | "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", 1323 | "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", 1324 | "dependencies": { 1325 | "camelcase-css": "^2.0.1" 1326 | }, 1327 | "engines": { 1328 | "node": "^12 || ^14 || >= 16" 1329 | }, 1330 | "funding": { 1331 | "type": "opencollective", 1332 | "url": "https://opencollective.com/postcss/" 1333 | }, 1334 | "peerDependencies": { 1335 | "postcss": "^8.4.21" 1336 | } 1337 | }, 1338 | "node_modules/postcss-load-config": { 1339 | "version": "4.0.2", 1340 | "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", 1341 | "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", 1342 | "funding": [ 1343 | { 1344 | "type": "opencollective", 1345 | "url": "https://opencollective.com/postcss/" 1346 | }, 1347 | { 1348 | "type": "github", 1349 | "url": "https://github.com/sponsors/ai" 1350 | } 1351 | ], 1352 | "dependencies": { 1353 | "lilconfig": "^3.0.0", 1354 | "yaml": "^2.3.4" 1355 | }, 1356 | "engines": { 1357 | "node": ">= 14" 1358 | }, 1359 | "peerDependencies": { 1360 | "postcss": ">=8.0.9", 1361 | "ts-node": ">=9.0.0" 1362 | }, 1363 | "peerDependenciesMeta": { 1364 | "postcss": { 1365 | "optional": true 1366 | }, 1367 | "ts-node": { 1368 | "optional": true 1369 | } 1370 | } 1371 | }, 1372 | "node_modules/postcss-load-config/node_modules/lilconfig": { 1373 | "version": "3.1.1", 1374 | "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.1.tgz", 1375 | "integrity": "sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==", 1376 | "engines": { 1377 | "node": ">=14" 1378 | }, 1379 | "funding": { 1380 | "url": "https://github.com/sponsors/antonk52" 1381 | } 1382 | }, 1383 | "node_modules/postcss-nested": { 1384 | "version": "6.0.1", 1385 | "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz", 1386 | "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==", 1387 | "dependencies": { 1388 | "postcss-selector-parser": "^6.0.11" 1389 | }, 1390 | "engines": { 1391 | "node": ">=12.0" 1392 | }, 1393 | "funding": { 1394 | "type": "opencollective", 1395 | "url": "https://opencollective.com/postcss/" 1396 | }, 1397 | "peerDependencies": { 1398 | "postcss": "^8.2.14" 1399 | } 1400 | }, 1401 | "node_modules/postcss-selector-parser": { 1402 | "version": "6.0.16", 1403 | "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.16.tgz", 1404 | "integrity": "sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==", 1405 | "dependencies": { 1406 | "cssesc": "^3.0.0", 1407 | "util-deprecate": "^1.0.2" 1408 | }, 1409 | "engines": { 1410 | "node": ">=4" 1411 | } 1412 | }, 1413 | "node_modules/postcss-value-parser": { 1414 | "version": "4.2.0", 1415 | "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", 1416 | "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" 1417 | }, 1418 | "node_modules/queue-microtask": { 1419 | "version": "1.2.3", 1420 | "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", 1421 | "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", 1422 | "funding": [ 1423 | { 1424 | "type": "github", 1425 | "url": "https://github.com/sponsors/feross" 1426 | }, 1427 | { 1428 | "type": "patreon", 1429 | "url": "https://www.patreon.com/feross" 1430 | }, 1431 | { 1432 | "type": "consulting", 1433 | "url": "https://feross.org/support" 1434 | } 1435 | ] 1436 | }, 1437 | "node_modules/ramda": { 1438 | "version": "0.29.1", 1439 | "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.29.1.tgz", 1440 | "integrity": "sha512-OfxIeWzd4xdUNxlWhgFazxsA/nl3mS4/jGZI5n00uWOoSSFRhC1b6gl6xvmzUamgmqELraWp0J/qqVlXYPDPyA==", 1441 | "funding": { 1442 | "type": "opencollective", 1443 | "url": "https://opencollective.com/ramda" 1444 | } 1445 | }, 1446 | "node_modules/react": { 1447 | "version": "18.2.0", 1448 | "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", 1449 | "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", 1450 | "dependencies": { 1451 | "loose-envify": "^1.1.0" 1452 | }, 1453 | "engines": { 1454 | "node": ">=0.10.0" 1455 | } 1456 | }, 1457 | "node_modules/react-dom": { 1458 | "version": "18.2.0", 1459 | "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", 1460 | "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", 1461 | "dependencies": { 1462 | "loose-envify": "^1.1.0", 1463 | "scheduler": "^0.23.0" 1464 | }, 1465 | "peerDependencies": { 1466 | "react": "^18.2.0" 1467 | } 1468 | }, 1469 | "node_modules/read-cache": { 1470 | "version": "1.0.0", 1471 | "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", 1472 | "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", 1473 | "dependencies": { 1474 | "pify": "^2.3.0" 1475 | } 1476 | }, 1477 | "node_modules/readdirp": { 1478 | "version": "3.6.0", 1479 | "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", 1480 | "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", 1481 | "dependencies": { 1482 | "picomatch": "^2.2.1" 1483 | }, 1484 | "engines": { 1485 | "node": ">=8.10.0" 1486 | } 1487 | }, 1488 | "node_modules/resolve": { 1489 | "version": "1.22.8", 1490 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", 1491 | "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", 1492 | "dependencies": { 1493 | "is-core-module": "^2.13.0", 1494 | "path-parse": "^1.0.7", 1495 | "supports-preserve-symlinks-flag": "^1.0.0" 1496 | }, 1497 | "bin": { 1498 | "resolve": "bin/resolve" 1499 | }, 1500 | "funding": { 1501 | "url": "https://github.com/sponsors/ljharb" 1502 | } 1503 | }, 1504 | "node_modules/reusify": { 1505 | "version": "1.0.4", 1506 | "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", 1507 | "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", 1508 | "engines": { 1509 | "iojs": ">=1.0.0", 1510 | "node": ">=0.10.0" 1511 | } 1512 | }, 1513 | "node_modules/run-parallel": { 1514 | "version": "1.2.0", 1515 | "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", 1516 | "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", 1517 | "funding": [ 1518 | { 1519 | "type": "github", 1520 | "url": "https://github.com/sponsors/feross" 1521 | }, 1522 | { 1523 | "type": "patreon", 1524 | "url": "https://www.patreon.com/feross" 1525 | }, 1526 | { 1527 | "type": "consulting", 1528 | "url": "https://feross.org/support" 1529 | } 1530 | ], 1531 | "dependencies": { 1532 | "queue-microtask": "^1.2.2" 1533 | } 1534 | }, 1535 | "node_modules/safer-buffer": { 1536 | "version": "2.1.2", 1537 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 1538 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", 1539 | "dev": true 1540 | }, 1541 | "node_modules/scheduler": { 1542 | "version": "0.23.2", 1543 | "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", 1544 | "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", 1545 | "dependencies": { 1546 | "loose-envify": "^1.1.0" 1547 | } 1548 | }, 1549 | "node_modules/shebang-command": { 1550 | "version": "2.0.0", 1551 | "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", 1552 | "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", 1553 | "dependencies": { 1554 | "shebang-regex": "^3.0.0" 1555 | }, 1556 | "engines": { 1557 | "node": ">=8" 1558 | } 1559 | }, 1560 | "node_modules/shebang-regex": { 1561 | "version": "3.0.0", 1562 | "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", 1563 | "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", 1564 | "engines": { 1565 | "node": ">=8" 1566 | } 1567 | }, 1568 | "node_modules/signal-exit": { 1569 | "version": "4.1.0", 1570 | "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", 1571 | "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", 1572 | "engines": { 1573 | "node": ">=14" 1574 | }, 1575 | "funding": { 1576 | "url": "https://github.com/sponsors/isaacs" 1577 | } 1578 | }, 1579 | "node_modules/source-map-js": { 1580 | "version": "1.2.0", 1581 | "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", 1582 | "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", 1583 | "engines": { 1584 | "node": ">=0.10.0" 1585 | } 1586 | }, 1587 | "node_modules/streamsearch": { 1588 | "version": "1.1.0", 1589 | "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", 1590 | "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", 1591 | "engines": { 1592 | "node": ">=10.0.0" 1593 | } 1594 | }, 1595 | "node_modules/string-width": { 1596 | "version": "5.1.2", 1597 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", 1598 | "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", 1599 | "dependencies": { 1600 | "eastasianwidth": "^0.2.0", 1601 | "emoji-regex": "^9.2.2", 1602 | "strip-ansi": "^7.0.1" 1603 | }, 1604 | "engines": { 1605 | "node": ">=12" 1606 | }, 1607 | "funding": { 1608 | "url": "https://github.com/sponsors/sindresorhus" 1609 | } 1610 | }, 1611 | "node_modules/string-width-cjs": { 1612 | "name": "string-width", 1613 | "version": "4.2.3", 1614 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", 1615 | "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 1616 | "dependencies": { 1617 | "emoji-regex": "^8.0.0", 1618 | "is-fullwidth-code-point": "^3.0.0", 1619 | "strip-ansi": "^6.0.1" 1620 | }, 1621 | "engines": { 1622 | "node": ">=8" 1623 | } 1624 | }, 1625 | "node_modules/string-width-cjs/node_modules/ansi-regex": { 1626 | "version": "5.0.1", 1627 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 1628 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 1629 | "engines": { 1630 | "node": ">=8" 1631 | } 1632 | }, 1633 | "node_modules/string-width-cjs/node_modules/emoji-regex": { 1634 | "version": "8.0.0", 1635 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 1636 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" 1637 | }, 1638 | "node_modules/string-width-cjs/node_modules/strip-ansi": { 1639 | "version": "6.0.1", 1640 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 1641 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 1642 | "dependencies": { 1643 | "ansi-regex": "^5.0.1" 1644 | }, 1645 | "engines": { 1646 | "node": ">=8" 1647 | } 1648 | }, 1649 | "node_modules/strip-ansi": { 1650 | "version": "7.1.0", 1651 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", 1652 | "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", 1653 | "dependencies": { 1654 | "ansi-regex": "^6.0.1" 1655 | }, 1656 | "engines": { 1657 | "node": ">=12" 1658 | }, 1659 | "funding": { 1660 | "url": "https://github.com/chalk/strip-ansi?sponsor=1" 1661 | } 1662 | }, 1663 | "node_modules/strip-ansi-cjs": { 1664 | "name": "strip-ansi", 1665 | "version": "6.0.1", 1666 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 1667 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 1668 | "dependencies": { 1669 | "ansi-regex": "^5.0.1" 1670 | }, 1671 | "engines": { 1672 | "node": ">=8" 1673 | } 1674 | }, 1675 | "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { 1676 | "version": "5.0.1", 1677 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 1678 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 1679 | "engines": { 1680 | "node": ">=8" 1681 | } 1682 | }, 1683 | "node_modules/styled-jsx": { 1684 | "version": "5.1.1", 1685 | "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz", 1686 | "integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==", 1687 | "dependencies": { 1688 | "client-only": "0.0.1" 1689 | }, 1690 | "engines": { 1691 | "node": ">= 12.0.0" 1692 | }, 1693 | "peerDependencies": { 1694 | "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0" 1695 | }, 1696 | "peerDependenciesMeta": { 1697 | "@babel/core": { 1698 | "optional": true 1699 | }, 1700 | "babel-plugin-macros": { 1701 | "optional": true 1702 | } 1703 | } 1704 | }, 1705 | "node_modules/sucrase": { 1706 | "version": "3.35.0", 1707 | "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", 1708 | "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", 1709 | "dependencies": { 1710 | "@jridgewell/gen-mapping": "^0.3.2", 1711 | "commander": "^4.0.0", 1712 | "glob": "^10.3.10", 1713 | "lines-and-columns": "^1.1.6", 1714 | "mz": "^2.7.0", 1715 | "pirates": "^4.0.1", 1716 | "ts-interface-checker": "^0.1.9" 1717 | }, 1718 | "bin": { 1719 | "sucrase": "bin/sucrase", 1720 | "sucrase-node": "bin/sucrase-node" 1721 | }, 1722 | "engines": { 1723 | "node": ">=16 || 14 >=14.17" 1724 | } 1725 | }, 1726 | "node_modules/supports-preserve-symlinks-flag": { 1727 | "version": "1.0.0", 1728 | "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", 1729 | "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", 1730 | "engines": { 1731 | "node": ">= 0.4" 1732 | }, 1733 | "funding": { 1734 | "url": "https://github.com/sponsors/ljharb" 1735 | } 1736 | }, 1737 | "node_modules/tailwindcss": { 1738 | "version": "3.4.1", 1739 | "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.1.tgz", 1740 | "integrity": "sha512-qAYmXRfk3ENzuPBakNK0SRrUDipP8NQnEY6772uDhflcQz5EhRdD7JNZxyrFHVQNCwULPBn6FNPp9brpO7ctcA==", 1741 | "dependencies": { 1742 | "@alloc/quick-lru": "^5.2.0", 1743 | "arg": "^5.0.2", 1744 | "chokidar": "^3.5.3", 1745 | "didyoumean": "^1.2.2", 1746 | "dlv": "^1.1.3", 1747 | "fast-glob": "^3.3.0", 1748 | "glob-parent": "^6.0.2", 1749 | "is-glob": "^4.0.3", 1750 | "jiti": "^1.19.1", 1751 | "lilconfig": "^2.1.0", 1752 | "micromatch": "^4.0.5", 1753 | "normalize-path": "^3.0.0", 1754 | "object-hash": "^3.0.0", 1755 | "picocolors": "^1.0.0", 1756 | "postcss": "^8.4.23", 1757 | "postcss-import": "^15.1.0", 1758 | "postcss-js": "^4.0.1", 1759 | "postcss-load-config": "^4.0.1", 1760 | "postcss-nested": "^6.0.1", 1761 | "postcss-selector-parser": "^6.0.11", 1762 | "resolve": "^1.22.2", 1763 | "sucrase": "^3.32.0" 1764 | }, 1765 | "bin": { 1766 | "tailwind": "lib/cli.js", 1767 | "tailwindcss": "lib/cli.js" 1768 | }, 1769 | "engines": { 1770 | "node": ">=14.0.0" 1771 | } 1772 | }, 1773 | "node_modules/thenify": { 1774 | "version": "3.3.1", 1775 | "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", 1776 | "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", 1777 | "dependencies": { 1778 | "any-promise": "^1.0.0" 1779 | } 1780 | }, 1781 | "node_modules/thenify-all": { 1782 | "version": "1.6.0", 1783 | "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", 1784 | "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", 1785 | "dependencies": { 1786 | "thenify": ">= 3.1.0 < 4" 1787 | }, 1788 | "engines": { 1789 | "node": ">=0.8" 1790 | } 1791 | }, 1792 | "node_modules/to-regex-range": { 1793 | "version": "5.0.1", 1794 | "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", 1795 | "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", 1796 | "dependencies": { 1797 | "is-number": "^7.0.0" 1798 | }, 1799 | "engines": { 1800 | "node": ">=8.0" 1801 | } 1802 | }, 1803 | "node_modules/tr46": { 1804 | "version": "0.0.3", 1805 | "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", 1806 | "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" 1807 | }, 1808 | "node_modules/ts-interface-checker": { 1809 | "version": "0.1.13", 1810 | "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", 1811 | "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==" 1812 | }, 1813 | "node_modules/tslib": { 1814 | "version": "2.6.2", 1815 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", 1816 | "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" 1817 | }, 1818 | "node_modules/typescript": { 1819 | "version": "5.3.3", 1820 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", 1821 | "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", 1822 | "bin": { 1823 | "tsc": "bin/tsc", 1824 | "tsserver": "bin/tsserver" 1825 | }, 1826 | "engines": { 1827 | "node": ">=14.17" 1828 | } 1829 | }, 1830 | "node_modules/undici-types": { 1831 | "version": "5.26.5", 1832 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", 1833 | "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" 1834 | }, 1835 | "node_modules/update-browserslist-db": { 1836 | "version": "1.0.16", 1837 | "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz", 1838 | "integrity": "sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==", 1839 | "funding": [ 1840 | { 1841 | "type": "opencollective", 1842 | "url": "https://opencollective.com/browserslist" 1843 | }, 1844 | { 1845 | "type": "tidelift", 1846 | "url": "https://tidelift.com/funding/github/npm/browserslist" 1847 | }, 1848 | { 1849 | "type": "github", 1850 | "url": "https://github.com/sponsors/ai" 1851 | } 1852 | ], 1853 | "dependencies": { 1854 | "escalade": "^3.1.2", 1855 | "picocolors": "^1.0.1" 1856 | }, 1857 | "bin": { 1858 | "update-browserslist-db": "cli.js" 1859 | }, 1860 | "peerDependencies": { 1861 | "browserslist": ">= 4.21.0" 1862 | } 1863 | }, 1864 | "node_modules/util-deprecate": { 1865 | "version": "1.0.2", 1866 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 1867 | "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" 1868 | }, 1869 | "node_modules/webidl-conversions": { 1870 | "version": "3.0.1", 1871 | "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", 1872 | "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" 1873 | }, 1874 | "node_modules/whatwg-url": { 1875 | "version": "5.0.0", 1876 | "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", 1877 | "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", 1878 | "dependencies": { 1879 | "tr46": "~0.0.3", 1880 | "webidl-conversions": "^3.0.0" 1881 | } 1882 | }, 1883 | "node_modules/which": { 1884 | "version": "2.0.2", 1885 | "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", 1886 | "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", 1887 | "dependencies": { 1888 | "isexe": "^2.0.0" 1889 | }, 1890 | "bin": { 1891 | "node-which": "bin/node-which" 1892 | }, 1893 | "engines": { 1894 | "node": ">= 8" 1895 | } 1896 | }, 1897 | "node_modules/wrap-ansi": { 1898 | "version": "8.1.0", 1899 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", 1900 | "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", 1901 | "dependencies": { 1902 | "ansi-styles": "^6.1.0", 1903 | "string-width": "^5.0.1", 1904 | "strip-ansi": "^7.0.1" 1905 | }, 1906 | "engines": { 1907 | "node": ">=12" 1908 | }, 1909 | "funding": { 1910 | "url": "https://github.com/chalk/wrap-ansi?sponsor=1" 1911 | } 1912 | }, 1913 | "node_modules/wrap-ansi-cjs": { 1914 | "name": "wrap-ansi", 1915 | "version": "7.0.0", 1916 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", 1917 | "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", 1918 | "dependencies": { 1919 | "ansi-styles": "^4.0.0", 1920 | "string-width": "^4.1.0", 1921 | "strip-ansi": "^6.0.0" 1922 | }, 1923 | "engines": { 1924 | "node": ">=10" 1925 | }, 1926 | "funding": { 1927 | "url": "https://github.com/chalk/wrap-ansi?sponsor=1" 1928 | } 1929 | }, 1930 | "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { 1931 | "version": "5.0.1", 1932 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 1933 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 1934 | "engines": { 1935 | "node": ">=8" 1936 | } 1937 | }, 1938 | "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { 1939 | "version": "4.3.0", 1940 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 1941 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 1942 | "dependencies": { 1943 | "color-convert": "^2.0.1" 1944 | }, 1945 | "engines": { 1946 | "node": ">=8" 1947 | }, 1948 | "funding": { 1949 | "url": "https://github.com/chalk/ansi-styles?sponsor=1" 1950 | } 1951 | }, 1952 | "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { 1953 | "version": "8.0.0", 1954 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 1955 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" 1956 | }, 1957 | "node_modules/wrap-ansi-cjs/node_modules/string-width": { 1958 | "version": "4.2.3", 1959 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", 1960 | "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 1961 | "dependencies": { 1962 | "emoji-regex": "^8.0.0", 1963 | "is-fullwidth-code-point": "^3.0.0", 1964 | "strip-ansi": "^6.0.1" 1965 | }, 1966 | "engines": { 1967 | "node": ">=8" 1968 | } 1969 | }, 1970 | "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { 1971 | "version": "6.0.1", 1972 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 1973 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 1974 | "dependencies": { 1975 | "ansi-regex": "^5.0.1" 1976 | }, 1977 | "engines": { 1978 | "node": ">=8" 1979 | } 1980 | }, 1981 | "node_modules/ws": { 1982 | "version": "8.17.0", 1983 | "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz", 1984 | "integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==", 1985 | "engines": { 1986 | "node": ">=10.0.0" 1987 | }, 1988 | "peerDependencies": { 1989 | "bufferutil": "^4.0.1", 1990 | "utf-8-validate": ">=5.0.2" 1991 | }, 1992 | "peerDependenciesMeta": { 1993 | "bufferutil": { 1994 | "optional": true 1995 | }, 1996 | "utf-8-validate": { 1997 | "optional": true 1998 | } 1999 | } 2000 | }, 2001 | "node_modules/yaml": { 2002 | "version": "2.4.2", 2003 | "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.2.tgz", 2004 | "integrity": "sha512-B3VqDZ+JAg1nZpaEmWtTXUlBneoGx6CPM9b0TENK6aoSu5t73dItudwdgmi6tHlIZZId4dZ9skcAQ2UbcyAeVA==", 2005 | "bin": { 2006 | "yaml": "bin.mjs" 2007 | }, 2008 | "engines": { 2009 | "node": ">= 14" 2010 | } 2011 | } 2012 | } 2013 | } 2014 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "dev": "next dev", 5 | "build": "next build", 6 | "start": "next start" 7 | }, 8 | "dependencies": { 9 | "@supabase/ssr": "latest", 10 | "@supabase/supabase-js": "latest", 11 | "autoprefixer": "10.4.17", 12 | "geist": "^1.2.1", 13 | "next": "latest", 14 | "postcss": "8.4.33", 15 | "react": "18.2.0", 16 | "react-dom": "18.2.0", 17 | "tailwindcss": "3.4.1", 18 | "typescript": "5.3.3" 19 | }, 20 | "devDependencies": { 21 | "@types/node": "20.11.5", 22 | "@types/react": "18.2.48", 23 | "@types/react-dom": "18.2.18", 24 | "encoding": "^0.1.13" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "model_manager" 3 | version = "0.1.0" 4 | description = "Default template for PDM package" 5 | authors = [ 6 | {name = "Arthur Chi", email = "arthur@openfoundry.ai"}, 7 | ] 8 | dependencies = [ 9 | "pyyaml>=6.0.1", 10 | "python-dotenv>=1.0.1", 11 | "rich>=13.7.1", 12 | "sagemaker>=2.218.1", 13 | "boto3>=1.34.99", 14 | "setuptools>=69.5.1", 15 | "huggingface-hub>=0.23.0", 16 | "pydantic>=2.7.1", 17 | "datasets>=2.19.1", 18 | "transformers>=4.40.2", 19 | "inquirer>=3.2.4", 20 | "inquirerpy>=0.3.4", 21 | "uvicorn>=0.29.0", 22 | "fastapi>=0.111.0", 23 | "litellm>=1.37.5", 24 | ] 25 | requires-python = ">=3.12" 26 | readme = "README.md" 27 | license = {text = "MIT"} 28 | 29 | [build-system] 30 | requires = ["pdm-backend"] 31 | build-backend = "pdm.backend" 32 | 33 | [tool.pdm] 34 | distribution = true 35 | 36 | [tool.pdm.dev-dependencies] 37 | test = [ 38 | "openai>=1.27.0", 39 | "pytest>=8.2.0", 40 | "httpx>=0.27.0", 41 | ] 42 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp==3.9.5 2 | aiosignal==1.3.1 3 | annotated-types==0.6.0 4 | anyio==4.3.0 5 | attrs==23.2.0 6 | blessed==1.20.0 7 | boto3==1.34.65 8 | botocore==1.34.65 9 | certifi==2024.2.2 10 | charset-normalizer==3.3.2 11 | click==8.1.7 12 | cloudpickle==2.2.1 13 | contextlib2==21.6.0 14 | datasets==2.19.0 15 | dill==0.3.8 16 | docker==7.0.0 17 | editor==1.6.6 18 | fastapi==0.110.1 19 | filelock==3.13.1 20 | frozenlist==1.4.1 21 | fsspec==2024.3.1 22 | google-pasta==0.2.0 23 | h11==0.14.0 24 | httptools==0.6.1 25 | huggingface-hub==0.21.4 26 | idna==3.7 27 | importlib-metadata==6.11.0 28 | inquirer==3.2.4 29 | inquirerpy==0.3.4 30 | jmespath==1.0.1 31 | jsonschema==4.21.1 32 | jsonschema-specifications==2023.12.1 33 | markdown-it-py==3.0.0 34 | mdurl==0.1.2 35 | multidict==6.0.5 36 | multiprocess==0.70.16 37 | numpy==1.26.4 38 | packaging==24.0 39 | pandas==2.2.1 40 | pathos==0.3.2 41 | pfzy==0.3.4 42 | platformdirs==4.2.0 43 | pox==0.3.4 44 | ppft==1.7.6.8 45 | prompt-toolkit==3.0.43 46 | protobuf==4.25.3 47 | psutil==5.9.8 48 | pyarrow==16.0.0 49 | pyarrow-hotfix==0.6 50 | pydantic==2.6.4 51 | pydantic-core==2.16.3 52 | pygments==2.17.2 53 | python-dateutil==2.9.0.post0 54 | python-dotenv==1.0.1 55 | pytz==2024.1 56 | pyyaml==6.0.1 57 | readchar==4.0.6 58 | referencing==0.34.0 59 | regex==2024.4.28 60 | requests==2.31.0 61 | rich==13.7.1 62 | rpds-py==0.18.0 63 | runs==1.2.2 64 | s3transfer==0.10.1 65 | safetensors==0.4.3 66 | sagemaker==2.218.0 67 | schema==0.7.5 68 | setuptools==69.5.1 69 | six==1.16.0 70 | smdebug-rulesconfig==1.0.1 71 | sniffio==1.3.1 72 | starlette==0.37.2 73 | tblib==3.0.0 74 | tokenizers==0.19.1 75 | tqdm==4.66.4 76 | transformers==4.40.1 77 | typing-extensions==4.10.0 78 | tzdata==2024.1 79 | urllib3==2.2.1 80 | uvicorn==0.29.0 81 | uvloop==0.19.0 82 | watchfiles==0.21.0 83 | wcwidth==0.2.13 84 | websockets==12.0 85 | xmod==1.8.1 86 | xxhash==3.4.1 87 | yarl==1.9.4 88 | zipp==3.18.1 89 | -------------------------------------------------------------------------------- /scripts/setup_role.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # This script creates a role named SageMakerRole 4 | # that can be used by SageMaker and has access to S3. 5 | 6 | ROLE_NAME=SageMakerRole 7 | 8 | # Creates a AWS policy for full access to sagemaker 9 | POLICY=arn:aws:iam::aws:policy/AmazonSageMakerFullAccess 10 | 11 | if `aws iam get-role --role-name ${ROLE_NAME} &> /dev/null` ; then 12 | echo "SAGEMAKER_ROLE=`aws iam get-role --role-name ${ROLE_NAME} | grep -Eo '"arn:aws:iam.*?[^\\]"'`" >> .env 13 | exit 14 | fi 15 | 16 | cat < /tmp/assume-role-policy-document.json 17 | { 18 | "Version": "2012-10-17", 19 | "Statement": [{ 20 | "Effect": "Allow", 21 | "Principal": { 22 | "Service": "sagemaker.amazonaws.com" 23 | }, 24 | "Action": "sts:AssumeRole" 25 | }] 26 | } 27 | EOF 28 | 29 | # Creates the role 30 | echo "SAGEMAKER_ROLE=`aws iam create-role --role-name ${ROLE_NAME} --assume-role-policy-document file:///tmp/assume-role-policy-document.json | grep -Eo '"arn:aws:iam.*?[^\\]"'`" >> .env 31 | 32 | # attaches the Sagemaker full access policy to the role 33 | aws iam attach-role-policy --policy-arn ${POLICY} --role-name ${ROLE_NAME} -------------------------------------------------------------------------------- /server.py: -------------------------------------------------------------------------------- 1 | import uvicorn 2 | import os 3 | from dotenv import dotenv_values 4 | from fastapi import FastAPI 5 | from src.config import get_config_for_endpoint, get_endpoints_for_model 6 | from src.sagemaker.resources import get_sagemaker_endpoint 7 | from src.sagemaker.query_endpoint import make_query_request 8 | from src.schemas.query import Query, ChatCompletion 9 | from src.session import session 10 | from litellm import completion 11 | 12 | os.environ["AWS_REGION_NAME"] = session.region_name 13 | app = FastAPI() 14 | 15 | 16 | class NotDeployedException(Exception): 17 | pass 18 | 19 | 20 | @app.get("/endpoint/{endpoint_name}") 21 | def get_endpoint(endpoint_name: str): 22 | return get_sagemaker_endpoint(endpoint_name) 23 | 24 | 25 | @app.post("/endpoint/{endpoint_name}/query") 26 | def query_endpoint(endpoint_name: str, query: Query): 27 | config = get_config_for_endpoint(endpoint_name) 28 | if query.context is None: 29 | query.context = '' 30 | 31 | # Support multi-model endpoints 32 | config = (config.deployment, config.models[0]) 33 | return make_query_request(endpoint_name, query, config) 34 | 35 | 36 | @app.post("/chat/completions") 37 | def chat_completion(chat_completion: ChatCompletion): 38 | model_id = chat_completion.model 39 | 40 | # Validate model is for completion tasks 41 | endpoints = get_endpoints_for_model(model_id) 42 | if len(endpoints) == 0: 43 | raise NotDeployedException 44 | 45 | messages = chat_completion.messages 46 | # Currently using the first available endpoint. 47 | endpoint_name = endpoints[0].deployment.endpoint_name 48 | 49 | res = completion( 50 | model=f"sagemaker/{endpoint_name}", 51 | messages=messages, 52 | temperature=0.9, 53 | hf_model_name=model_id, 54 | ) 55 | 56 | return res 57 | 58 | 59 | if __name__ == "__main__": 60 | uvicorn.run(app, host="0.0.0.0", port=8000) 61 | -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | python3 -m ensurepip --upgrade 3 | make 4 | source venv/bin/activate 5 | if ! command -v aws &> /dev/null 6 | then 7 | OS="$(uname -s)" 8 | case "${OS}" in 9 | Linux*) 10 | curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" 11 | unzip awscliv2.zip 12 | sudo ./aws/install 13 | ;; 14 | Darwin*) 15 | curl "https://awscli.amazonaws.com/AWSCLIV2.pkg" -o "AWSCLIV2.pkg" 16 | sudo installer -pkg AWSCLIV2.pkg -target / 17 | ;; 18 | *) 19 | echo "Unsupported OS: ${OS}. See https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html" 20 | exit 1 21 | ;; 22 | esac 23 | fi 24 | aws configure set region us-east-1 && aws configure 25 | touch .env 26 | if ! grep -q "SAGEMAKER_ROLE" .env 27 | then 28 | bash ./scripts/setup_role.sh 29 | fi 30 | source venv/bin/activate -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openfoundry-ai/model_manager/34f9ffc3e1962aa87ca088203535d8d0506b4e7b/src/__init__.py -------------------------------------------------------------------------------- /src/config.py: -------------------------------------------------------------------------------- 1 | import glob 2 | import yaml 3 | from src.yaml import dumper 4 | from src.schemas.model import Model 5 | from src.schemas.deployment import Deployment 6 | from typing import Dict, Tuple, List, NamedTuple, Optional 7 | 8 | 9 | class ModelDeployment(NamedTuple): 10 | deployment: Deployment 11 | models: List[Model] 12 | 13 | 14 | def get_deployment_configs(path: Optional[str] = None) -> List[ModelDeployment]: 15 | if path is None: 16 | path = "./configs/*.yaml" 17 | 18 | configurations = glob.glob(path) 19 | configs = [] 20 | 21 | for configuration in configurations: 22 | with open(configuration) as config: 23 | configuration = yaml.safe_load(config) 24 | if configuration is None: 25 | continue 26 | 27 | # Filter out training configs 28 | if configuration.get('deployment') is None: 29 | continue 30 | 31 | deployment = configuration['deployment'] 32 | models = configuration['models'] 33 | configs.append(ModelDeployment( 34 | deployment=deployment, models=models)) 35 | 36 | return configs 37 | 38 | 39 | def get_endpoints_for_model(model_id: str, path: Optional[str] = None) -> List[ModelDeployment]: 40 | configs = get_deployment_configs(path) 41 | endpoints = [] 42 | for config in configs: 43 | models = [model.id for model in config.models] 44 | if model_id in models: 45 | # TODO: Check if endpoint is still active 46 | endpoints.append(config) 47 | return endpoints 48 | 49 | 50 | def get_config_for_endpoint(endpoint_name: str) -> Optional[ModelDeployment]: 51 | configs = get_deployment_configs() 52 | for config in configs: 53 | if config.deployment.endpoint_name == endpoint_name: 54 | return config 55 | return None 56 | 57 | 58 | def write_config(deployment: Deployment, model: Model): 59 | with open(f"./configs/{deployment.endpoint_name}.yaml", 'w') as config: 60 | out = { 61 | "deployment": deployment, 62 | "models": [model], 63 | } 64 | config.write(yaml.dump(out, Dumper=dumper)) 65 | return 66 | -------------------------------------------------------------------------------- /src/console.py: -------------------------------------------------------------------------------- 1 | from rich.console import Console 2 | console = Console() 3 | -------------------------------------------------------------------------------- /src/huggingface/__init__.py: -------------------------------------------------------------------------------- 1 | from enum import StrEnum 2 | from huggingface_hub import HfApi 3 | 4 | 5 | class HuggingFaceTask(StrEnum): 6 | AudioClassification = "audio-classification" 7 | AutomaticSpeechRecognition = "automatic-speech-recognition" 8 | Conversational = "conversational" 9 | DepthEstimation = "depth-estimation" 10 | DocumentQuestionAnswering = "document-question-answering" 11 | FeatureExtraction = "feature-extraction" 12 | FillMask = "fill-mask" 13 | ImageClassification = "image-classification" 14 | ImageFeatureExtraction = "image-feature-extraction" 15 | ImageSegmentation = "image-segmentation" 16 | ImageToImage = "image-to-image" 17 | ImageToText = "image-to-text" 18 | MaskGeneration = "mask-generation" 19 | ObjectDetection = "object-detection" 20 | QuestionAnswering = "question-answering" 21 | Summarization = "summarization" 22 | TableQuestionAnswering = "table-question-answering" 23 | Text2TextGeneration = "text2text-generation" 24 | TextClassification = "text-classification" 25 | TextGeneration = "text-generation" 26 | TextToAudio = "text-to-audio" 27 | TokenClassification = "token-classification" 28 | Translation = "translation" 29 | TranslationXXtoYY = "translation_xx_to_yy" 30 | VideoClassification = "video-classification" 31 | VisualQuestionAnswering = "visual-question-answering" 32 | ZeroShotClassification = "zero-shot-classification" 33 | ZeroShotImageClassification = "zero-shot-image-classification" 34 | ZeroShotAudioClassification = "zero-shot-audio-classification" 35 | ZeroShotObjectDetection = "zero-shot-object-detection" 36 | 37 | 38 | AVAILABLE_PIPELINES = ["feature-extraction", 39 | "text-classification", "token-classification", "question-answering", 40 | "table-question-answering", "fill-mask", "summarization", "translation", 41 | "text2text-generation", "text-generation", "zero-shot-classification", "conversational", 42 | "image-classification", "translation_XX_to_YY"] 43 | 44 | hf_api = HfApi() 45 | -------------------------------------------------------------------------------- /src/huggingface/hf_hub_api.py: -------------------------------------------------------------------------------- 1 | from dotenv import dotenv_values 2 | from src.console import console 3 | from src.huggingface import hf_api 4 | from src.schemas.model import Model 5 | from src.utils.rich_utils import print_error 6 | 7 | HUGGING_FACE_HUB_TOKEN = dotenv_values(".env").get("HUGGING_FACE_HUB_KEY") 8 | 9 | 10 | def get_hf_task(model: Model): 11 | task = None 12 | try: 13 | model_info = hf_api.model_info( 14 | model.id, token=HUGGING_FACE_HUB_TOKEN) 15 | task = model_info.pipeline_tag 16 | if model_info.transformers_info is not None and model_info.transformers_info.pipeline_tag is not None: 17 | task = model_info.transformers_info.pipeline_tag 18 | except Exception: 19 | # better error handling for auth 20 | console.print_exception() 21 | print_error("Model not found, please try another.") 22 | return 23 | return task 24 | -------------------------------------------------------------------------------- /src/main.py: -------------------------------------------------------------------------------- 1 | import inquirer 2 | import logging 3 | import threading 4 | from InquirerPy import prompt 5 | from src.sagemaker import EC2Instance 6 | from src.sagemaker.create_model import deploy_model 7 | from src.sagemaker.delete_model import delete_sagemaker_model 8 | from src.sagemaker.resources import list_sagemaker_endpoints, select_instance, list_service_quotas_async 9 | from src.sagemaker.query_endpoint import make_query_request 10 | from src.sagemaker.search_jumpstart_models import search_sagemaker_jumpstart_model 11 | from src.utils.rich_utils import print_error, print_success 12 | from src.schemas.deployment import Deployment, Destination 13 | from src.schemas.model import Model, ModelSource 14 | from src.schemas.query import Query 15 | from src.config import get_config_for_endpoint 16 | from enum import StrEnum 17 | from rich import print 18 | 19 | 20 | class Actions(StrEnum): 21 | LIST = "Show active model endpoints" 22 | DEPLOY = "Deploy a model endpoint" 23 | DELETE = "Delete a model endpoint" 24 | QUERY = "Query a model endpoint" 25 | EXIT = "Quit" 26 | # TRAIN = "fine tune a model" 27 | 28 | 29 | def main(args, loglevel): 30 | logging.basicConfig(format="%(levelname)s: %(message)s", level=loglevel) 31 | 32 | print("[magenta]Model Manager by OpenFoundry.") 33 | print("[magenta]Star us on Github ☆! [blue]https://github.com/openfoundry-ai/model_manager") 34 | 35 | # list_service_quotas is a pretty slow API and it's paginated. 36 | # Use async here and store the result in instances 37 | instances = [] 38 | instance_thread = threading.Thread( 39 | target=list_service_quotas_async, args=[instances]) 40 | instance_thread.start() 41 | 42 | while True: 43 | active_endpoints = list_sagemaker_endpoints() 44 | questions = [ 45 | inquirer.List( 46 | 'action', 47 | message="What would you like to do?", 48 | choices=[e.value for e in Actions] 49 | ) 50 | ] 51 | answers = inquirer.prompt(questions) 52 | if (answers is None): 53 | break 54 | 55 | action = answers['action'] 56 | 57 | match action: 58 | case Actions.LIST: 59 | if len(active_endpoints) != 0: 60 | [print(f"[blue]{endpoint['EndpointName']}[/blue] running on an [green]{endpoint['InstanceType']}[/green] instance") 61 | for endpoint in active_endpoints] 62 | print('\n') 63 | else: 64 | print_error('No active endpoints.\n') 65 | case Actions.DEPLOY: 66 | build_and_deploy_model(instances, instance_thread) 67 | case Actions.DELETE: 68 | if (len(active_endpoints) == 0): 69 | print_success("No Endpoints to delete!") 70 | continue 71 | 72 | questions = [ 73 | inquirer.Checkbox('endpoints', 74 | message="Which endpoints would you like to delete? (space to select)", 75 | choices=[endpoint['EndpointName'] 76 | for endpoint in active_endpoints] 77 | ) 78 | ] 79 | answers = inquirer.prompt(questions) 80 | if (answers is None): 81 | continue 82 | 83 | endpoints_to_delete = answers['endpoints'] 84 | delete_sagemaker_model(endpoints_to_delete) 85 | case Actions.QUERY: 86 | if (len(active_endpoints) == 0): 87 | print_success("No Endpoints to query!") 88 | continue 89 | 90 | questions = [ 91 | { 92 | "type": "list", 93 | "message": "Which endpoint would you like to query?", 94 | "name": 'endpoint', 95 | "choices": [endpoint['EndpointName'] for endpoint in active_endpoints] 96 | }, 97 | { 98 | "type": "input", 99 | "message": "What would you like to query?", 100 | "name": 'query' 101 | } 102 | ] 103 | answers = prompt(questions) 104 | if (answers is None): 105 | continue 106 | 107 | endpoint = answers['endpoint'] 108 | query = Query(query=answers['query']) 109 | config = get_config_for_endpoint(endpoint) 110 | 111 | # support multi-model endpoints 112 | config = (config.deployment, config.models[0]) 113 | make_query_request(endpoint, query, config) 114 | case Actions.EXIT: 115 | quit() 116 | 117 | print_success("Goodbye!") 118 | 119 | 120 | class ModelType(StrEnum): 121 | SAGEMAKER = "Deploy a Sagemaker model" 122 | HUGGINGFACE = "Deploy a Hugging Face model" 123 | CUSTOM = "Deploy a custom model" 124 | 125 | 126 | def build_and_deploy_model(instances, instance_thread): 127 | questions = [ 128 | inquirer.List( 129 | 'model_type', 130 | message="Choose a model type:", 131 | choices=[model_type.value for model_type in ModelType] 132 | ) 133 | ] 134 | answers = inquirer.prompt(questions) 135 | if answers is None: 136 | return 137 | 138 | model_type = answers['model_type'] 139 | 140 | match model_type: 141 | case ModelType.SAGEMAKER: 142 | models = [] 143 | while len(models) == 0: 144 | models = search_sagemaker_jumpstart_model() 145 | if models is None: 146 | quit() 147 | 148 | questions = [ 149 | { 150 | "type": "fuzzy", 151 | "name": "model_id", 152 | "message": "Choose a model. Search by task (e.g. eqa) or model name (e.g. llama):", 153 | "choices": models, 154 | "match_exact": True, 155 | } 156 | ] 157 | answers = prompt(questions) 158 | if (answers is None): 159 | return 160 | 161 | model_id, model_version = answers['model_id'], "2.*" 162 | instance_thread.join() 163 | instance_type = select_instance(instances) 164 | 165 | model = Model( 166 | id=model_id, 167 | model_version=model_version, 168 | source=ModelSource.Sagemaker 169 | ) 170 | 171 | deployment = Deployment( 172 | instance_type=instance_type, 173 | destination=Destination.AWS, 174 | num_gpus=4 if instance_type == EC2Instance.LARGE else 1 175 | ) 176 | 177 | predictor = deploy_model(deployment=deployment, model=model) 178 | case ModelType.HUGGINGFACE: 179 | questions = [ 180 | inquirer.Text( 181 | 'model_id', 182 | message="Enter the exact model name from huggingface.co (e.g. google-bert/bert-base-uncased)", 183 | ) 184 | ] 185 | answers = inquirer.prompt(questions) 186 | if answers is None: 187 | return 188 | 189 | model_id = answers['model_id'] 190 | instance_thread.join() 191 | instance_type = select_instance(instances) 192 | 193 | model = Model( 194 | id=model_id, 195 | source=ModelSource.HuggingFace 196 | ) 197 | 198 | deployment = Deployment( 199 | instance_type=instance_type, 200 | destination=Destination.AWS, 201 | ) 202 | 203 | predictor = deploy_model(deployment=deployment, model=model) 204 | 205 | case ModelType.CUSTOM: 206 | questions = [ 207 | { 208 | "type": "input", 209 | "message": "What is the local path or S3 URI of the model?", 210 | "name": "path" 211 | }, 212 | { 213 | "type": "input", 214 | "message": "What is the base model that was fine tuned? (e.g. google-bert/bert-base-uncased)", 215 | "name": "base_model" 216 | } 217 | ] 218 | answers = prompt(questions) 219 | local_path = answers['path'] 220 | base_model = answers['base_model'] 221 | 222 | instance_thread.join() 223 | instance_type = select_instance(instances) 224 | 225 | model = Model( 226 | id=base_model, 227 | source=ModelSource.Custom, 228 | location=local_path 229 | ) 230 | 231 | deployment = Deployment( 232 | instance_type=instance_type, 233 | destination=Destination.AWS 234 | ) 235 | deploy_model(deployment=deployment, model=model) 236 | -------------------------------------------------------------------------------- /src/sagemaker/__init__.py: -------------------------------------------------------------------------------- 1 | from enum import StrEnum 2 | 3 | 4 | class EC2Instance(StrEnum): 5 | SMALL = "ml.m5.xlarge" 6 | MEDIUM = "ml.p3.2xlarge" 7 | LARGE = "ml.g5.12xlarge" 8 | 9 | 10 | class SagemakerTask(StrEnum): 11 | AutomaticSpeechRecognition = "asr" 12 | AudioEmbedding = "audioembedding" 13 | Classification = "classification" 14 | Depth2img = "depth2img" 15 | ExtractiveQuestionAnswering = "eqa" 16 | FillMask = "fillmask" 17 | ImageClassification = "ic" 18 | ImageEmbedding = "icembedding" 19 | ImageGeneration = "imagegeneration" 20 | Inpainting = "inpainting" 21 | ImageSegmentation = "is" 22 | LLM = "llm" 23 | NamedEntityRecognition = "ner" 24 | ObjectDetection = "od" 25 | Od1 = "od1" 26 | Regression = "regression" 27 | SemanticSegmentation = "semseg" 28 | SentenceSimilarity = "sentencesimilarity" 29 | SentencePairClassification = "spc" 30 | Summarization = "summarization" 31 | TabTransformerClassification = "tabtransformerclassification" 32 | TabTransformerRegression = "tabtransformerregression" 33 | TextClassification = "tc" 34 | TcEmbedding = "tcembedding" 35 | Text2text = "text2text" 36 | TextEmbedding = "textembedding" 37 | TextGeneration = "textgeneration" 38 | TextGeneration1 = "textgeneration1" 39 | TextGeneration2 = "textgeneration2" 40 | TextGenerationJP = "textgenerationjp" 41 | TextGenerationNeuron = "textgenerationneuron" 42 | Translation = "translation" 43 | Txt2img = "txt2img" 44 | Upscaling = "upscaling" 45 | ZeroShotTextClassification = "zstc" 46 | 47 | @staticmethod 48 | def list(): 49 | return list(map(lambda c: c.value, SagemakerTask)) 50 | -------------------------------------------------------------------------------- /src/sagemaker/create_model.py: -------------------------------------------------------------------------------- 1 | import json 2 | from dotenv import dotenv_values 3 | from rich.table import Table 4 | from sagemaker import image_uris, model_uris, script_uris 5 | from sagemaker.huggingface import get_huggingface_llm_image_uri 6 | from sagemaker.huggingface.model import HuggingFaceModel 7 | from sagemaker.jumpstart.model import JumpStartModel 8 | from sagemaker.jumpstart.estimator import JumpStartEstimator 9 | from sagemaker.model import Model 10 | from sagemaker.predictor import Predictor 11 | from sagemaker.s3 import S3Uploader 12 | from src.config import write_config 13 | from src.schemas.model import Model, ModelSource 14 | from src.schemas.deployment import Deployment 15 | from src.session import session, sagemaker_session 16 | from src.console import console 17 | from src.utils.aws_utils import construct_s3_uri, is_s3_uri 18 | from src.utils.rich_utils import print_error, print_success 19 | from src.utils.model_utils import get_unique_endpoint_name, get_model_and_task 20 | from src.huggingface import HuggingFaceTask 21 | from src.huggingface.hf_hub_api import get_hf_task 22 | 23 | HUGGING_FACE_HUB_TOKEN = dotenv_values(".env").get("HUGGING_FACE_HUB_KEY") 24 | SAGEMAKER_ROLE = dotenv_values(".env")["SAGEMAKER_ROLE"] 25 | 26 | 27 | # TODO: Consolidate 28 | def deploy_model(deployment: Deployment, model: Model): 29 | match model.source: 30 | case ModelSource.HuggingFace: 31 | deploy_huggingface_model(deployment, model) 32 | case ModelSource.Sagemaker: 33 | create_and_deploy_jumpstart_model(deployment, model) 34 | case ModelSource.Custom: 35 | deploy_custom_huggingface_model(deployment, model) 36 | 37 | 38 | def deploy_huggingface_model(deployment: Deployment, model: Model): 39 | region_name = session.region_name 40 | task = get_hf_task(model) 41 | model.task = task 42 | env = { 43 | 'HF_MODEL_ID': model.id, 44 | 'HF_TASK': task, 45 | } 46 | 47 | if HUGGING_FACE_HUB_TOKEN is not None: 48 | env['HUGGING_FACE_HUB_TOKEN'] = HUGGING_FACE_HUB_TOKEN 49 | 50 | image_uri = None 51 | if deployment.num_gpus: 52 | env['SM_NUM_GPUS'] = json.dumps(deployment.num_gpus) 53 | 54 | if deployment.quantization: 55 | env['HF_MODEL_QUANTIZE'] = deployment.quantization 56 | 57 | if task == HuggingFaceTask.TextGeneration: 58 | # use TGI imageq if llm. 59 | image_uri = get_huggingface_llm_image_uri( 60 | "huggingface", 61 | version="1.4.2" 62 | ) 63 | 64 | huggingface_model = HuggingFaceModel( 65 | env=env, 66 | role=SAGEMAKER_ROLE, 67 | transformers_version="4.37", 68 | pytorch_version="2.1", 69 | py_version="py310", 70 | image_uri=image_uri 71 | ) 72 | 73 | endpoint_name = get_unique_endpoint_name( 74 | model.id, deployment.endpoint_name) 75 | 76 | deployment.endpoint_name = endpoint_name 77 | 78 | console.log( 79 | "Deploying model to AWS. [magenta]This may take up to 10 minutes for very large models.[/magenta] See full logs here:") 80 | console.print( 81 | f"https://{region_name}.console.aws.amazon.com/cloudwatch/home#logsV2:log-groups/log-group/$252Faws$252Fsagemaker$252FEndpoints$252F{endpoint_name}") 82 | 83 | with console.status("[bold green]Deploying model...") as status: 84 | table = Table(show_header=False, header_style="magenta") 85 | table.add_column("Resource", style="dim") 86 | table.add_column("Value", style="blue") 87 | table.add_row("model", model.id) 88 | table.add_row("EC2 instance type", deployment.instance_type) 89 | table.add_row("Number of instances", str( 90 | deployment.instance_count)) 91 | table.add_row("task", task) 92 | console.print(table) 93 | 94 | try: 95 | predictor = huggingface_model.deploy( 96 | initial_instance_count=deployment.instance_count, 97 | instance_type=deployment.instance_type, 98 | endpoint_name=endpoint_name, 99 | ) 100 | except Exception: 101 | console.print_exception() 102 | quit() 103 | 104 | print_success( 105 | f"{model.id} is now up and running at the endpoint [blue]{predictor.endpoint_name}") 106 | 107 | write_config(deployment, model) 108 | return predictor 109 | 110 | 111 | def deploy_custom_huggingface_model(deployment: Deployment, model: Model): 112 | region_name = session.region_name 113 | if model.location is None: 114 | print_error("Missing model source location.") 115 | return 116 | 117 | s3_path = model.location 118 | if not is_s3_uri(model.location): 119 | # Local file. Upload to s3 before deploying 120 | bucket = sagemaker_session.default_bucket() 121 | s3_path = construct_s3_uri(bucket, f"models/{model.id}") 122 | with console.status(f"[bold green]Uploading custom {model.id} model to S3 at {s3_path}...") as status: 123 | try: 124 | s3_path = S3Uploader.upload( 125 | model.location, s3_path) 126 | except Exception: 127 | print_error("[red] Model failed to upload to S3") 128 | 129 | endpoint_name = get_unique_endpoint_name( 130 | model.id, deployment.endpoint_name) 131 | 132 | deployment.endpoint_name = endpoint_name 133 | model.task = get_model_and_task(model.id)['task'] 134 | 135 | console.log( 136 | "Deploying model to AWS. [magenta]This may take up to 10 minutes for very large models.[/magenta] See full logs here:") 137 | console.print( 138 | f"https://{region_name}.console.aws.amazon.com/cloudwatch/home#logsV2:log-groups/log-group/$252Faws$252Fsagemaker$252FEndpoints$252F{endpoint_name}") 139 | 140 | # create Hugging Face Model Class 141 | huggingface_model = HuggingFaceModel( 142 | # path to your trained sagemaker model 143 | model_data=s3_path, 144 | role=SAGEMAKER_ROLE, # iam role with permissions to create an Endpoint 145 | transformers_version="4.37", 146 | pytorch_version="2.1", 147 | py_version="py310", 148 | ) 149 | 150 | with console.status("[bold green]Deploying model...") as status: 151 | table = Table(show_header=False, header_style="magenta") 152 | table.add_column("Resource", style="dim") 153 | table.add_column("Value", style="blue") 154 | table.add_row("S3 Path", s3_path) 155 | table.add_row("EC2 instance type", deployment.instance_type) 156 | table.add_row("Number of instances", str( 157 | deployment.instance_count)) 158 | console.print(table) 159 | 160 | try: 161 | predictor = huggingface_model.deploy( 162 | initial_instance_count=deployment.instance_count, 163 | instance_type=deployment.instance_type, 164 | endpoint_name=endpoint_name 165 | ) 166 | except Exception: 167 | console.print_exception() 168 | quit() 169 | 170 | print_success( 171 | f"Custom {model.id} is now up and running at the endpoint [blue]{predictor.endpoint_name}") 172 | 173 | write_config(deployment, model) 174 | return predictor 175 | 176 | 177 | def create_and_deploy_jumpstart_model(deployment: Deployment, model: Model): 178 | region_name = session.region_name 179 | endpoint_name = get_unique_endpoint_name( 180 | model.id, deployment.endpoint_name) 181 | deployment.endpoint_name = endpoint_name 182 | model.task = get_model_and_task(model.id)['task'] 183 | 184 | console.log( 185 | "Deploying model to AWS. [magenta]This may take up to 10 minutes for very large models.[/magenta] See full logs here:") 186 | 187 | console.print( 188 | f"https://{region_name}.console.aws.amazon.com/cloudwatch/home#logsV2:log-groups/log-group/$252Faws$252Fsagemaker$252FEndpoints$252F{endpoint_name}") 189 | 190 | with console.status("[bold green]Deploying model...") as status: 191 | table = Table(show_header=False, header_style="magenta") 192 | table.add_column("Resource", style="dim") 193 | table.add_column("Value", style="blue") 194 | table.add_row("model", model.id) 195 | table.add_row("EC2 instance type", deployment.instance_type) 196 | table.add_row("Number of instances", str( 197 | deployment.instance_count)) 198 | console.print(table) 199 | 200 | jumpstart_model = JumpStartModel( 201 | model_id=model.id, instance_type=deployment.instance_type, role=SAGEMAKER_ROLE) 202 | 203 | # Attempt to deploy to AWS 204 | try: 205 | predictor = jumpstart_model.deploy( 206 | initial_instance_count=deployment.instance_count, 207 | instance_type=deployment.instance_type, 208 | endpoint_name=endpoint_name, 209 | accept_eula=True 210 | ) 211 | pass 212 | except Exception: 213 | console.print_exception() 214 | quit() 215 | 216 | write_config(deployment, model) 217 | print_success( 218 | f"{model.id} is now up and running at the endpoint [blue]{predictor.endpoint_name}") 219 | 220 | return predictor 221 | -------------------------------------------------------------------------------- /src/sagemaker/delete_model.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | from rich import print 3 | from src.utils.rich_utils import print_success 4 | from typing import List 5 | 6 | 7 | def delete_sagemaker_model(endpoint_names: List[str] = None): 8 | sagemaker_client = boto3.client('sagemaker') 9 | 10 | if len(endpoint_names) == 0: 11 | print_success("No Endpoints to delete!") 12 | return 13 | 14 | # Add validation / error handling 15 | for endpoint in endpoint_names: 16 | print(f"Deleting [blue]{endpoint}") 17 | sagemaker_client.delete_endpoint(EndpointName=endpoint) 18 | -------------------------------------------------------------------------------- /src/sagemaker/fine_tune_model.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import sagemaker 4 | from botocore.exceptions import ClientError 5 | from datasets import load_dataset 6 | from rich import print 7 | from rich.table import Table 8 | from sagemaker.jumpstart.estimator import JumpStartEstimator 9 | from src.console import console 10 | from src.schemas.model import Model, ModelSource 11 | from src.schemas.training import Training 12 | from src.session import sagemaker_session 13 | from src.utils.aws_utils import is_s3_uri 14 | from src.utils.rich_utils import print_success, print_error 15 | from transformers import AutoTokenizer 16 | 17 | from dotenv import load_dotenv 18 | load_dotenv() 19 | SAGEMAKER_ROLE = os.environ.get("SAGEMAKER_ROLE") 20 | 21 | 22 | def prep_hf_data(s3_bucket: str, dataset_name_or_path: str, model: Model): 23 | train_dataset, test_dataset = load_dataset( 24 | dataset_name_or_path, split=["train", "test"]) 25 | tokenizer = AutoTokenizer.from_pretrained(model.id) 26 | 27 | def tokenize(batch): 28 | return tokenizer(batch["text"], padding="max_length", truncation=True) 29 | 30 | # tokenize train and test datasets 31 | train_dataset = train_dataset.map(tokenize, batched=True) 32 | test_dataset = test_dataset.map(tokenize, batched=True) 33 | 34 | # set dataset format for PyTorch 35 | train_dataset = train_dataset.rename_column("label", "labels") 36 | train_dataset.set_format( 37 | "torch", columns=["input_ids", "attention_mask", "labels"]) 38 | test_dataset = test_dataset.rename_column("label", "labels") 39 | test_dataset.set_format( 40 | "torch", columns=["input_ids", "attention_mask", "labels"]) 41 | 42 | # save train_dataset to s3 43 | training_input_path = f's3://{s3_bucket}/datasets/train' 44 | train_dataset.save_to_disk(training_input_path) 45 | 46 | # save test_dataset to s3 47 | test_input_path = f's3://{s3_bucket}/datasets/test' 48 | test_dataset.save_to_disk(test_input_path) 49 | 50 | return training_input_path, test_input_path 51 | 52 | 53 | def train_model(training: Training, model: Model, estimator): 54 | # TODO: Accept hf datasets or local paths to upload to s3 55 | if not is_s3_uri(training.training_input_path): 56 | raise Exception("Training data needs to be uploaded to s3") 57 | 58 | # TODO: Implement training, validation, and test split or accept a directory of files 59 | training_dataset_s3_path = training.training_input_path 60 | 61 | table = Table(show_header=False, header_style="magenta") 62 | table.add_column("Resource", style="dim") 63 | table.add_column("Value", style="blue") 64 | table.add_row("model", model.id) 65 | table.add_row("model_version", model.version) 66 | table.add_row("base_model_uri", estimator.model_uri) 67 | table.add_row("image_uri", estimator.image_uri) 68 | table.add_row("EC2 instance type", training.instance_type) 69 | table.add_row("Number of instances", str(training.instance_count)) 70 | console.print(table) 71 | 72 | estimator.fit({"training": training_dataset_s3_path}) 73 | 74 | predictor = estimator.deploy( 75 | initial_instance_count=training.instance_count, instance_type=training.instance_type) 76 | 77 | print_success( 78 | f"Trained model {model.id} is now up and running at the endpoint [blue]{predictor.endpoint_name}") 79 | 80 | 81 | def fine_tune_model(training: Training, model: Model): 82 | estimator = None 83 | match model.source: 84 | case ModelSource.Sagemaker: 85 | hyperparameters = get_hyperparameters_for_model(training, model) 86 | estimator = JumpStartEstimator( 87 | model_id=model.id, 88 | model_version=model.version, 89 | instance_type=training.instance_type, 90 | instance_count=training.instance_count, 91 | output_path=training.output_path, 92 | environment={"accept_eula": "true"}, 93 | role=SAGEMAKER_ROLE, 94 | sagemaker_session=sagemaker_session, 95 | hyperparameters=hyperparameters 96 | ) 97 | case ModelSource.HuggingFace: 98 | raise NotImplementedError 99 | case ModelSource.Custom: 100 | raise NotImplementedError 101 | 102 | try: 103 | print_success("Enqueuing training job") 104 | res = train_model(training, model, estimator) 105 | except ClientError as e: 106 | logging.error(e) 107 | print_error("Training job enqueue fail") 108 | return False 109 | 110 | 111 | def get_hyperparameters_for_model(training: Training, model: Model): 112 | hyperparameters = sagemaker.hyperparameters.retrieve_default( 113 | model_id=model.id, model_version=model.version) 114 | 115 | if training.hyperparameters is not None: 116 | hyperparameters.update( 117 | (k, v) for k, v in training.hyperparameters.model_dump().items() if v is not None) 118 | return hyperparameters 119 | -------------------------------------------------------------------------------- /src/sagemaker/query_endpoint.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import json 3 | import inquirer 4 | from InquirerPy import prompt 5 | from sagemaker.huggingface.model import HuggingFacePredictor 6 | from src.config import ModelDeployment 7 | from src.console import console 8 | from src.sagemaker import SagemakerTask 9 | from src.huggingface import HuggingFaceTask 10 | from src.utils.model_utils import get_model_and_task, is_sagemaker_model, get_text_generation_hyperpameters 11 | from src.utils.rich_utils import print_error 12 | from src.schemas.deployment import Deployment 13 | from src.schemas.model import Model 14 | from src.schemas.query import Query 15 | from src.session import sagemaker_session 16 | from typing import Dict, Tuple, Optional 17 | 18 | 19 | def make_query_request(endpoint_name: str, query: Query, config: Tuple[Deployment, Model]): 20 | if is_sagemaker_model(endpoint_name, config): 21 | return query_sagemaker_endpoint(endpoint_name, query, config) 22 | else: 23 | return query_hugging_face_endpoint(endpoint_name, query, config) 24 | 25 | 26 | def parse_response(query_response): 27 | model_predictions = json.loads(query_response['Body'].read()) 28 | probabilities, labels, predicted_label = model_predictions[ 29 | 'probabilities'], model_predictions['labels'], model_predictions['predicted_label'] 30 | return probabilities, labels, predicted_label 31 | 32 | 33 | def query_hugging_face_endpoint(endpoint_name: str, user_query: Query, config: Tuple[Deployment, Model]): 34 | task = get_model_and_task(endpoint_name, config)['task'] 35 | predictor = HuggingFacePredictor(endpoint_name=endpoint_name, 36 | sagemaker_session=sagemaker_session) 37 | 38 | query = user_query.query 39 | context = user_query.context 40 | 41 | input = {"inputs": query} 42 | if task is not None and task == HuggingFaceTask.QuestionAnswering: 43 | if context is None: 44 | questions = [{ 45 | "type": "input", "message": "What context would you like to provide?:", "name": "context"}] 46 | answers = prompt(questions) 47 | context = answers.get('context', '') 48 | 49 | if not context: 50 | raise Exception("Must provide context for question-answering") 51 | 52 | input = {} 53 | input['context'] = answers['context'] 54 | input['question'] = query 55 | 56 | if task is not None and task == HuggingFaceTask.TextGeneration: 57 | parameters = get_text_generation_hyperpameters(config, user_query) 58 | input['parameters'] = parameters 59 | 60 | if task is not None and task == HuggingFaceTask.ZeroShotClassification: 61 | if context is None: 62 | questions = [ 63 | inquirer.Text('labels', 64 | message="What labels would you like to use? (comma separated values)?", 65 | ) 66 | ] 67 | answers = inquirer.prompt(questions) 68 | context = answers.get('labels', '') 69 | 70 | if not context: 71 | raise Exception( 72 | "Must provide labels for zero shot text classification") 73 | 74 | labels = context.split(',') 75 | input = json.dumps({ 76 | "sequences": query, 77 | "candidate_labels": labels 78 | }) 79 | 80 | try: 81 | result = predictor.predict(input) 82 | except Exception: 83 | console.print_exception() 84 | quit() 85 | 86 | print(result) 87 | return result 88 | 89 | 90 | def query_sagemaker_endpoint(endpoint_name: str, user_query: Query, config: Tuple[Deployment, Model]): 91 | client = boto3.client('runtime.sagemaker') 92 | task = get_model_and_task(endpoint_name, config)['task'] 93 | 94 | if task not in [ 95 | SagemakerTask.ExtractiveQuestionAnswering, 96 | SagemakerTask.TextClassification, 97 | SagemakerTask.SentenceSimilarity, 98 | SagemakerTask.SentencePairClassification, 99 | SagemakerTask.Summarization, 100 | SagemakerTask.NamedEntityRecognition, 101 | SagemakerTask.TextEmbedding, 102 | SagemakerTask.TcEmbedding, 103 | SagemakerTask.TextGeneration, 104 | SagemakerTask.TextGeneration1, 105 | SagemakerTask.TextGeneration2, 106 | SagemakerTask.Translation, 107 | SagemakerTask.FillMask, 108 | SagemakerTask.ZeroShotTextClassification 109 | ]: 110 | print_error(""" 111 | Querying this model type inside of Model Manager isn’t yet supported. 112 | You can query it directly through the API endpoint - see here for documentation on how to do this: 113 | https://docs.aws.amazon.com/sagemaker/latest/APIReference/API_runtime_InvokeEndpoint.html 114 | """) 115 | raise Exception("Unsupported") 116 | 117 | # MIME content type varies per deployment 118 | content_type = "application/x-text" 119 | accept_type = "application/json;verbose" 120 | 121 | # Depending on the task, input needs to be formatted differently. 122 | # e.g. question-answering needs to have {question: , context: } 123 | query = user_query.query 124 | context = user_query.context 125 | input = query.encode("utf-8") 126 | match task: 127 | case SagemakerTask.ExtractiveQuestionAnswering: 128 | if context is None: 129 | questions = [ 130 | { 131 | 'type': 'input', 132 | 'name': 'context', 133 | 'message': "What context would you like to provide?", 134 | } 135 | ] 136 | answers = prompt(questions) 137 | context = answers.get("context", '') 138 | 139 | if not context: 140 | raise Exception("Must provide context for question-answering") 141 | 142 | content_type = "application/list-text" 143 | input = json.dumps([query, context]).encode("utf-8") 144 | 145 | case SagemakerTask.SentencePairClassification: 146 | if context is None: 147 | questions = [ 148 | inquirer.Text('context', 149 | message="What sentence would you like to compare against?", 150 | ) 151 | ] 152 | answers = inquirer.prompt(questions) 153 | context = answers.get("context", '') 154 | if not context: 155 | raise Exception( 156 | "Must provide a second sentence for sentence pair classification") 157 | 158 | content_type = "application/list-text" 159 | input = json.dumps([query, context]).encode("utf-8") 160 | case SagemakerTask.ZeroShotTextClassification: 161 | if context is None: 162 | questions = [ 163 | inquirer.Text('labels', 164 | message="What labels would you like to use? (comma separated values)?", 165 | ) 166 | ] 167 | answers = inquirer.prompt(questions) 168 | context = answers.get('labels', '') 169 | 170 | if not context: 171 | raise Exception( 172 | "must provide labels for zero shot text classification") 173 | labels = context.split(',') 174 | 175 | content_type = "application/json" 176 | input = json.dumps({ 177 | "sequences": query, 178 | "candidate_labels": labels, 179 | }).encode("utf-8") 180 | case SagemakerTask.TextGeneration: 181 | parameters = get_text_generation_hyperpameters(config, user_query) 182 | input = json.dumps({ 183 | "inputs": query, 184 | "parameters": parameters, 185 | }).encode("utf-8") 186 | content_type = "application/json" 187 | 188 | try: 189 | response = client.invoke_endpoint( 190 | EndpointName=endpoint_name, ContentType=content_type, Body=input, Accept=accept_type) 191 | except Exception: 192 | console.print_exception() 193 | quit() 194 | 195 | model_predictions = json.loads(response['Body'].read()) 196 | print(model_predictions) 197 | return model_predictions 198 | 199 | 200 | def test(endpoint_name: str): 201 | text1 = 'astonishing ... ( frames ) profound ethical and philosophical questions in the form of dazzling pop entertainment' 202 | text2 = 'simply stupid , irrelevant and deeply , truly , bottomlessly cynical ' 203 | 204 | for text in [text1, text2]: 205 | query_sagemaker_endpoint(endpoint_name, text.encode('utf-8')) 206 | -------------------------------------------------------------------------------- /src/sagemaker/resources.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | from functools import lru_cache 3 | from InquirerPy import inquirer 4 | from src.console import console 5 | from src.sagemaker import EC2Instance 6 | from src.config import get_config_for_endpoint 7 | from src.utils.format import format_sagemaker_endpoint, format_python_dict 8 | from typing import List, Tuple, Dict, Optional 9 | 10 | 11 | def list_sagemaker_endpoints(filter_str: str = None) -> List[str]: 12 | sagemaker_client = boto3.client('sagemaker') 13 | 14 | endpoints = sagemaker_client.list_endpoints()['Endpoints'] 15 | if filter_str is not None: 16 | endpoints = list(filter(lambda x: filter_str == 17 | x['EndpointName'], endpoints)) 18 | 19 | for endpoint in endpoints: 20 | endpoint_config = sagemaker_client.describe_endpoint_config( 21 | EndpointConfigName=endpoint['EndpointName'])['ProductionVariants'][0] 22 | endpoint['InstanceType'] = endpoint_config['InstanceType'] 23 | return endpoints 24 | 25 | 26 | def get_sagemaker_endpoint(endpoint_name: str) -> Optional[Dict[str, Optional[Dict]]]: 27 | endpoints = list_sagemaker_endpoints(endpoint_name) 28 | if not endpoints: 29 | return None 30 | 31 | endpoint = format_sagemaker_endpoint(endpoints[0]) 32 | 33 | config = get_config_for_endpoint(endpoint_name) 34 | if config is None: 35 | return {'deployment': endpoint, 'model': None} 36 | 37 | deployment = format_python_dict(config.deployment.model_dump()) 38 | formatted_models = [] 39 | for model in config.models: 40 | model = format_python_dict(model.model_dump()) 41 | formatted_models.append(model) 42 | 43 | # Merge the endpoint dict with our config 44 | deployment = {**endpoint, **deployment} 45 | 46 | return { 47 | 'deployment': deployment, 48 | 'models': formatted_models, 49 | } 50 | 51 | 52 | @lru_cache 53 | def list_service_quotas() -> List[Tuple[str, int]]: 54 | """ Gets a list of EC2 instances for inference """ 55 | 56 | client = boto3.client('service-quotas') 57 | quotas = [] 58 | try: 59 | response = client.list_service_quotas( 60 | ServiceCode="sagemaker", 61 | MaxResults=100, 62 | ) 63 | next_token = response.get('NextToken') 64 | quotas = response['Quotas'] 65 | while next_token is not None: 66 | response = client.list_service_quotas( 67 | ServiceCode="sagemaker", 68 | NextToken=next_token, 69 | ) 70 | quotas.extend(response['Quotas']) 71 | next_token = response.get('NextToken') 72 | except client.exceptions.AccessDeniedException as error: 73 | console.print( 74 | "[red]User does not have access to Service Quotas. Grant access via IAM to get the list of available instances") 75 | return [] 76 | 77 | # TODO: Filter for different usages 78 | available_instances = list(filter(lambda x: 'endpoint usage' in x[0] and x[1] > 0, [ 79 | (quota['QuotaName'], quota['Value']) for quota in quotas])) 80 | 81 | # Clean up quota names 82 | available_instances = [(instance[0].split(" ")[0], instance[1]) 83 | for instance in available_instances] 84 | return available_instances 85 | 86 | 87 | def list_service_quotas_async(instances=[]): 88 | """ Wrapper to allow access to list in threading """ 89 | instances = instances.extend(list_service_quotas()) 90 | return instances 91 | 92 | 93 | def select_instance(available_instances=None): 94 | choices = [instance[0] for instance in available_instances] or [ 95 | instance for instance in EC2Instance] 96 | instance = inquirer.fuzzy( 97 | message="Choose an instance size (note: ml.m5.xlarge available by default; you must request quota from AWS to use other instance types):", 98 | choices=choices, 99 | default="ml.m5." 100 | ).execute() 101 | if instance is None: 102 | return 103 | return instance 104 | -------------------------------------------------------------------------------- /src/sagemaker/search_jumpstart_models.py: -------------------------------------------------------------------------------- 1 | import inquirer 2 | from enum import StrEnum, auto 3 | from sagemaker.jumpstart.notebook_utils import list_jumpstart_models 4 | from src.utils.rich_utils import print_error 5 | from src.session import session, sagemaker_session 6 | 7 | 8 | class Frameworks(StrEnum): 9 | huggingface = auto() 10 | meta = auto() 11 | model = auto() 12 | tensorflow = auto() 13 | pytorch = auto() 14 | # autogluon 15 | # catboost 16 | # lightgbm 17 | mxnet = auto() 18 | # sklearn 19 | # xgboost 20 | 21 | 22 | def search_sagemaker_jumpstart_model(): 23 | questions = [ 24 | inquirer.List('framework', 25 | message="Which framework would you like to use?", 26 | choices=[framework.value for framework in Frameworks] 27 | ), 28 | ] 29 | answers = inquirer.prompt(questions) 30 | if answers is None: 31 | return 32 | filter_value = "framework == {}".format(answers["framework"]) 33 | 34 | models = list_jumpstart_models(filter=filter_value, 35 | region=session.region_name, sagemaker_session=sagemaker_session) 36 | return models 37 | -------------------------------------------------------------------------------- /src/schemas/__init__.py: -------------------------------------------------------------------------------- 1 | import src.schemas.deployment 2 | import src.schemas.model 3 | import src.schemas.query 4 | import src.schemas.training 5 | -------------------------------------------------------------------------------- /src/schemas/deployment.py: -------------------------------------------------------------------------------- 1 | import yaml 2 | from src.yaml import loader, dumper 3 | from typing import Optional 4 | from enum import StrEnum 5 | from pydantic import BaseModel 6 | 7 | 8 | class Destination(StrEnum): 9 | AWS = "aws" 10 | # AZURE = "azure" 11 | # GCP = "gcp" 12 | 13 | 14 | class Deployment(BaseModel): 15 | destination: Destination 16 | instance_type: str 17 | endpoint_name: Optional[str] = None 18 | instance_count: Optional[int] = 1 19 | num_gpus: Optional[int] = None 20 | quantization: Optional[str] = None 21 | 22 | 23 | def deployment_representer(dumper: yaml.SafeDumper, deployment: Deployment) -> yaml.nodes.MappingNode: 24 | return dumper.represent_mapping("!Deployment", { 25 | "destination": deployment.destination.value, 26 | "instance_type": deployment.instance_type, 27 | "endpoint_name": deployment.endpoint_name, 28 | "instance_count": deployment.instance_count, 29 | "num_gpus": deployment.num_gpus, 30 | "quantization": deployment.quantization, 31 | }) 32 | 33 | 34 | def deployment_constructor(loader: yaml.SafeLoader, node: yaml.nodes.ScalarNode) -> Deployment: 35 | return Deployment(**loader.construct_mapping(node)) 36 | 37 | 38 | dumper.add_representer(Deployment, deployment_representer) 39 | loader.add_constructor(u'!Deployment', deployment_constructor) 40 | -------------------------------------------------------------------------------- /src/schemas/model.py: -------------------------------------------------------------------------------- 1 | import yaml 2 | from src.yaml import loader, dumper 3 | from typing import Optional, Union, Dict 4 | from enum import StrEnum 5 | from src.huggingface import HuggingFaceTask 6 | from src.sagemaker import SagemakerTask 7 | from pydantic import BaseModel 8 | 9 | 10 | class ModelSource(StrEnum): 11 | HuggingFace = "huggingface" 12 | Sagemaker = "sagemaker" 13 | Custom = "custom" 14 | 15 | 16 | class Model(BaseModel): 17 | id: str 18 | source: ModelSource 19 | task: Optional[Union[HuggingFaceTask, SagemakerTask]] = None 20 | version: Optional[str] = None 21 | location: Optional[str] = None 22 | predict: Optional[Dict[str, str]] = None 23 | 24 | 25 | def model_representer(dumper: yaml.SafeDumper, model: Model) -> yaml.nodes.MappingNode: 26 | return dumper.represent_mapping("!Model", { 27 | "id": model.id, 28 | "source": model.source.value, 29 | "task": model.task, 30 | "version": model.version, 31 | "location": model.location, 32 | "predict": model.predict, 33 | }) 34 | 35 | 36 | def model_constructor(loader: yaml.SafeLoader, node: yaml.nodes.MappingNode) -> Model: 37 | return Model(**loader.construct_mapping(node)) 38 | 39 | 40 | dumper.add_representer(Model, model_representer) 41 | loader.add_constructor(u'!Model', model_constructor) 42 | -------------------------------------------------------------------------------- /src/schemas/query.py: -------------------------------------------------------------------------------- 1 | 2 | from pydantic import BaseModel 3 | from typing import Optional, List, Literal 4 | 5 | 6 | class QueryParameters(BaseModel): 7 | max_length: Optional[int] = None 8 | max_new_tokens: Optional[int] = None 9 | repetition_penalty: Optional[float] = None 10 | temperature: Optional[float] = None 11 | top_k: Optional[float] = None 12 | top_p: Optional[float] = None 13 | 14 | 15 | class Query(BaseModel): 16 | query: str 17 | context: Optional[str] = None 18 | parameters: Optional[QueryParameters] = None 19 | 20 | 21 | class ChatMessage(BaseModel): 22 | role: Literal["user", "assistant", "function", "system"] 23 | content: str 24 | 25 | 26 | class ChatCompletion(BaseModel): 27 | model: str 28 | messages: List[ChatMessage] 29 | -------------------------------------------------------------------------------- /src/schemas/training.py: -------------------------------------------------------------------------------- 1 | import yaml 2 | from pydantic import BaseModel 3 | from typing import Optional 4 | from src.schemas.deployment import Destination 5 | from src.yaml import loader, dumper 6 | 7 | 8 | class Hyperparameters(BaseModel): 9 | epochs: Optional[int] = None 10 | per_device_train_batch_size: Optional[int] = None 11 | learning_rate: Optional[float] = None 12 | 13 | 14 | class Training(BaseModel): 15 | destination: Destination 16 | instance_type: str 17 | instance_count: int 18 | training_input_path: str 19 | output_path: Optional[str] = None 20 | hyperparameters: Optional[Hyperparameters] = None 21 | 22 | 23 | def training_representer(dumper: yaml.SafeDumper, training: Training) -> yaml.nodes.MappingNode: 24 | return dumper.represent_mapping("!Training", { 25 | "destination": training.destination.value, 26 | "instance_type": training.instance_type, 27 | "instance_count": training.instance_count, 28 | "output_path": training.output_path, 29 | "training_input_path": training.training_input_path, 30 | "hyperparameters": training.hyperparameters, 31 | }) 32 | 33 | 34 | def training_constructor(loader: yaml.SafeLoader, node: yaml.nodes.MappingNode) -> Training: 35 | return Training(**loader.construct_mapping(node)) 36 | 37 | 38 | def hyperparameters_constructor(loader: yaml.SafeLoader, node: yaml.nodes.MappingNode) -> Hyperparameters: 39 | return Hyperparameters(**loader.construct_mapping(node)) 40 | 41 | 42 | dumper.add_representer(Training, training_representer) 43 | loader.add_constructor(u'!Training', training_constructor) 44 | loader.add_constructor(u'!Hyperparameters', hyperparameters_constructor) 45 | -------------------------------------------------------------------------------- /src/session.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import sagemaker 3 | 4 | session = boto3.session.Session() 5 | sagemaker_session = sagemaker.session.Session(boto_session=session) 6 | -------------------------------------------------------------------------------- /src/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openfoundry-ai/model_manager/34f9ffc3e1962aa87ca088203535d8d0506b4e7b/src/utils/__init__.py -------------------------------------------------------------------------------- /src/utils/aws_utils.py: -------------------------------------------------------------------------------- 1 | def construct_s3_uri(bucket: str, prefix: str) -> str: 2 | return f"s3://{bucket}/{prefix}" 3 | 4 | 5 | def is_s3_uri(path: str) -> bool: 6 | return path.startswith("s3://") 7 | -------------------------------------------------------------------------------- /src/utils/format.py: -------------------------------------------------------------------------------- 1 | from typing import Dict 2 | 3 | 4 | def to_camel_case(snake_str: str) -> str: 5 | return "".join(x.capitalize() for x in snake_str.lower().split("_")) 6 | 7 | 8 | def to_lower_camel_case(snake_str: str) -> str: 9 | # We capitalize the first letter of each component except the first one 10 | # with the 'capitalize' method and join them together. 11 | camel_string = to_camel_case(snake_str) 12 | return lower_first(camel_string) 13 | 14 | 15 | def lower_first(string): 16 | return string[0].lower() + string[1:] 17 | 18 | 19 | def format_sagemaker_endpoint(output: Dict[str, str]) -> Dict[str, str]: 20 | return {lower_first(key): val for key, val in output.items()} 21 | 22 | 23 | def format_python_dict(output: Dict[str, str]): 24 | return {to_lower_camel_case(key): val for key, val in output.items()} 25 | -------------------------------------------------------------------------------- /src/utils/model_utils.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from difflib import SequenceMatcher 3 | from dotenv import dotenv_values 4 | from huggingface_hub import HfApi 5 | from src.utils.rich_utils import print_error 6 | from src.sagemaker import SagemakerTask 7 | from src.schemas.deployment import Deployment 8 | from src.schemas.model import Model, ModelSource 9 | from src.schemas.query import Query 10 | from src.session import sagemaker_session 11 | from typing import Dict, Tuple, Optional 12 | HUGGING_FACE_HUB_TOKEN = dotenv_values(".env").get("HUGGING_FACE_HUB_KEY") 13 | 14 | 15 | def get_unique_endpoint_name(model_id: str, endpoint_name: str = None): 16 | dt_string = datetime.datetime.now().strftime("%Y%m%d%H%M") 17 | 18 | if not endpoint_name: 19 | # Endpoint name must be < 63 characters 20 | model_string = model_id.replace( 21 | "/", "--").replace("_", "-").replace(".", "")[:50] 22 | return f"{model_string}-{dt_string}" 23 | else: 24 | return f"{endpoint_name[:50]}-{dt_string}" 25 | 26 | 27 | def is_sagemaker_model(endpoint_name: str, config: Optional[Tuple[Deployment, Model]] = None) -> bool: 28 | if config is not None: 29 | _, model = config 30 | task = get_sagemaker_model_and_task(model.id)['task'] 31 | 32 | # check task for custom sagemaker models 33 | return model.source == ModelSource.Sagemaker or task in SagemakerTask.list() 34 | 35 | # fallback 36 | return endpoint_name.find("--") == -1 37 | 38 | 39 | def is_custom_model(endpoint_name: str) -> bool: 40 | return endpoint_name.startswith('custom') 41 | 42 | 43 | def get_sagemaker_model_and_task(endpoint_or_model_name: str): 44 | components = endpoint_or_model_name.split('-') 45 | framework, task = components[:2] 46 | model_id = '-'.join(components[:-1]) 47 | return { 48 | 'model_id': model_id, 49 | 'task': task, 50 | } 51 | 52 | 53 | def get_hugging_face_pipeline_task(model_name: str): 54 | hf_api = HfApi() 55 | try: 56 | model_info = hf_api.model_info( 57 | model_name, token=HUGGING_FACE_HUB_TOKEN) 58 | task = model_info.pipeline_tag 59 | except Exception: 60 | print_error("Model not found, please try another.") 61 | return None 62 | 63 | return task 64 | 65 | 66 | def get_model_and_task(endpoint_or_model_name: str, config: Optional[Tuple[Deployment, Model]] = None) -> Dict[str, str]: 67 | if config is not None: 68 | _, model = config 69 | return { 70 | 'model_id': model.id, 71 | 'task': model.task 72 | } 73 | 74 | if (is_sagemaker_model(endpoint_or_model_name)): 75 | return get_sagemaker_model_and_task(endpoint_or_model_name) 76 | else: 77 | model_id = get_model_name_from_hugging_face_endpoint( 78 | endpoint_or_model_name) 79 | task = get_hugging_face_pipeline_task(model_id) 80 | return { 81 | 'model_id': model_id, 82 | 'task': task 83 | } 84 | 85 | 86 | def get_model_name_from_hugging_face_endpoint(endpoint_name: str): 87 | endpoint_name = endpoint_name.removeprefix("custom-") 88 | endpoint_name = endpoint_name.replace("--", "/") 89 | author, rest = endpoint_name.split("/") 90 | 91 | # remove datetime 92 | split = rest.split('-') 93 | fuzzy_model_name = '-'.join(split[:-1]) 94 | 95 | # get first token 96 | search_term = fuzzy_model_name.split('-')[0] 97 | 98 | hf_api = HfApi() 99 | results = hf_api.list_models(search=search_term, author=author) 100 | 101 | # find results that closest match our fuzzy model name 102 | results_to_diff = {} 103 | for result in results: 104 | results_to_diff[result.id] = SequenceMatcher( 105 | None, result.id, f"{author}/{fuzzy_model_name}").ratio() 106 | 107 | return max(results_to_diff, key=results_to_diff.get) 108 | 109 | 110 | def get_text_generation_hyperpameters(config: Optional[Tuple[Deployment, Model]], query: Query = None): 111 | if query.parameters is not None: 112 | return query.parameters.model_dump() 113 | 114 | if config is not None and config[1].predict is not None: 115 | return config[1].predict 116 | 117 | # Defaults 118 | return { 119 | "max_new_tokens": 250, 120 | "top_p": 0.9, 121 | "temperature": 0.9, 122 | } 123 | -------------------------------------------------------------------------------- /src/utils/rich_utils.py: -------------------------------------------------------------------------------- 1 | from rich import print 2 | 3 | 4 | def print_model(text: str): 5 | print(f"[blue] {text}") 6 | 7 | 8 | def print_error(text: str): 9 | print(f"[red] {text}") 10 | 11 | 12 | def print_success(text: str): 13 | print(f"[green] {text}") 14 | -------------------------------------------------------------------------------- /src/yaml.py: -------------------------------------------------------------------------------- 1 | import yaml 2 | 3 | loader = yaml.SafeLoader 4 | dumper = yaml.SafeDumper 5 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./app/**/*.{js,ts,jsx,tsx,mdx}", 5 | "./components/**/*.{js,ts,jsx,tsx,mdx}", 6 | ], 7 | theme: { 8 | extend: { 9 | colors: { 10 | background: "hsl(var(--background))", 11 | foreground: "hsl(var(--foreground))", 12 | btn: { 13 | background: "hsl(var(--btn-background))", 14 | "background-hover": "hsl(var(--btn-background-hover))", 15 | }, 16 | }, 17 | }, 18 | }, 19 | plugins: [], 20 | }; 21 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openfoundry-ai/model_manager/34f9ffc3e1962aa87ca088203535d8d0506b4e7b/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_openai_proxy.py: -------------------------------------------------------------------------------- 1 | import httpx 2 | from openai import OpenAI, DefaultHttpxClient 3 | 4 | 5 | def test_openai_proxy(): 6 | client = OpenAI( 7 | api_key="test-key", 8 | base_url="http://127.0.0.1:8000/", 9 | http_client=DefaultHttpxClient( 10 | proxy="http://127.0.0.1:8000/", 11 | transport=httpx.HTTPTransport(local_address="0.0.0.0"), 12 | ) 13 | ) 14 | chat_completion = client.chat.completions.create( 15 | model="meta-llama/Meta-Llama-3-8B", 16 | messages=[ 17 | { 18 | "role": "system", 19 | "content": "You are a helpful assistant.", 20 | }, 21 | { 22 | "role": "user", 23 | "content": "Say this is a test", 24 | }, 25 | ], 26 | ) 27 | print(chat_completion) 28 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ], 22 | "paths": { 23 | "@/*": ["./*"] 24 | } 25 | }, 26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 27 | "exclude": ["node_modules"] 28 | } 29 | -------------------------------------------------------------------------------- /utils/supabase/client.ts: -------------------------------------------------------------------------------- 1 | import { createBrowserClient } from "@supabase/ssr"; 2 | 3 | export const createClient = () => 4 | createBrowserClient( 5 | process.env.NEXT_PUBLIC_SUPABASE_URL!, 6 | process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, 7 | ); 8 | -------------------------------------------------------------------------------- /utils/supabase/middleware.ts: -------------------------------------------------------------------------------- 1 | import { createServerClient, type CookieOptions } from "@supabase/ssr"; 2 | import { type NextRequest, NextResponse } from "next/server"; 3 | 4 | export const updateSession = async (request: NextRequest) => { 5 | // This `try/catch` block is only here for the interactive tutorial. 6 | // Feel free to remove once you have Supabase connected. 7 | try { 8 | // Create an unmodified response 9 | let response = NextResponse.next({ 10 | request: { 11 | headers: request.headers, 12 | }, 13 | }); 14 | 15 | const supabase = createServerClient( 16 | process.env.NEXT_PUBLIC_SUPABASE_URL!, 17 | process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, 18 | { 19 | cookies: { 20 | get(name: string) { 21 | return request.cookies.get(name)?.value; 22 | }, 23 | set(name: string, value: string, options: CookieOptions) { 24 | // If the cookie is updated, update the cookies for the request and response 25 | request.cookies.set({ 26 | name, 27 | value, 28 | ...options, 29 | }); 30 | response = NextResponse.next({ 31 | request: { 32 | headers: request.headers, 33 | }, 34 | }); 35 | response.cookies.set({ 36 | name, 37 | value, 38 | ...options, 39 | }); 40 | }, 41 | remove(name: string, options: CookieOptions) { 42 | // If the cookie is removed, update the cookies for the request and response 43 | request.cookies.set({ 44 | name, 45 | value: "", 46 | ...options, 47 | }); 48 | response = NextResponse.next({ 49 | request: { 50 | headers: request.headers, 51 | }, 52 | }); 53 | response.cookies.set({ 54 | name, 55 | value: "", 56 | ...options, 57 | }); 58 | }, 59 | }, 60 | }, 61 | ); 62 | 63 | // This will refresh session if expired - required for Server Components 64 | // https://supabase.com/docs/guides/auth/server-side/nextjs 65 | await supabase.auth.getUser(); 66 | 67 | return response; 68 | } catch (e) { 69 | // If you are here, a Supabase client could not be created! 70 | // This is likely because you have not set up environment variables. 71 | // Check out http://localhost:3000 for Next Steps. 72 | return NextResponse.next({ 73 | request: { 74 | headers: request.headers, 75 | }, 76 | }); 77 | } 78 | }; 79 | -------------------------------------------------------------------------------- /utils/supabase/server.ts: -------------------------------------------------------------------------------- 1 | import { createServerClient, type CookieOptions } from "@supabase/ssr"; 2 | import { cookies } from "next/headers"; 3 | 4 | export const createClient = () => { 5 | const cookieStore = cookies(); 6 | 7 | return createServerClient( 8 | process.env.NEXT_PUBLIC_SUPABASE_URL!, 9 | process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, 10 | { 11 | cookies: { 12 | get(name: string) { 13 | return cookieStore.get(name)?.value; 14 | }, 15 | set(name: string, value: string, options: CookieOptions) { 16 | try { 17 | cookieStore.set({ name, value, ...options }); 18 | } catch (error) { 19 | // The `set` method was called from a Server Component. 20 | // This can be ignored if you have middleware refreshing 21 | // user sessions. 22 | } 23 | }, 24 | remove(name: string, options: CookieOptions) { 25 | try { 26 | cookieStore.set({ name, value: "", ...options }); 27 | } catch (error) { 28 | // The `delete` method was called from a Server Component. 29 | // This can be ignored if you have middleware refreshing 30 | // user sessions. 31 | } 32 | }, 33 | }, 34 | }, 35 | ); 36 | }; 37 | --------------------------------------------------------------------------------