├── .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 | 
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 |
24 | About Model Manager
25 |
26 |
27 | Getting Started
28 |
32 |
33 | Using Model Manager
34 | What we're working on next
35 | Known issues
36 | Contributing
37 | License
38 | Contact
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 | 
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 | 
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 | 
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 | 
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 | 
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 |
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 |
17 | {isPending ? pendingText : children}
18 |
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 |
21 |
22 | Model Manager
23 | {isSupabaseConnected &&
}
24 |
25 |
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 |
25 |
26 | Model Manager
27 |
28 |
29 |
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 |
28 |
29 | ) : (
30 |
34 | Login
35 |
36 | );
37 | }
38 |
--------------------------------------------------------------------------------
/components/Header.tsx:
--------------------------------------------------------------------------------
1 | export default function Header() {
2 | return (
3 |
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 |
--------------------------------------------------------------------------------