diff --git a/daras_ai_v2/manage_api_keys_widget.py b/daras_ai_v2/manage_api_keys_widget.py index d1a5fd8a4..282fda4d7 100644 --- a/daras_ai_v2/manage_api_keys_widget.py +++ b/daras_ai_v2/manage_api_keys_widget.py @@ -1,6 +1,8 @@ import typing import gooey_gui as gui +from django.contrib.humanize.templatetags.humanize import naturaltime + from api_keys.models import ApiKey from app_users.models import AppUser from daras_ai_v2 import icons @@ -25,28 +27,57 @@ def manage_api_keys(workspace: "Workspace", user: AppUser): api_keys = list(workspace.api_keys.order_by("-created_at")) - table_area = gui.div() + table_area = gui.div( + className="table-responsive text-nowrap container-margin-reset" + ) - if gui.button("+ Create new secret key"): + if gui.button("+ Create new API Key"): api_key = generate_new_api_key(workspace=workspace, user=user) api_keys.insert(0, api_key) with table_area: - import pandas as pd - - gui.table( - pd.DataFrame.from_records( - columns=["Secret Key (Preview)", "Created At", "Created By"], - data=[ - ( - api_key.preview, - api_key.created_at.strftime("%B %d, %Y at %I:%M:%S %p %Z"), - api_key.created_by and api_key.created_by.full_name() or "", - ) - for api_key in api_keys - ], - ), - ) + with gui.tag("table", className="table table-striped"): + with gui.tag("thead"), gui.tag("tr"): + with gui.tag("th"): + gui.write("Gooey.AI Key") + with gui.tag("th"): + gui.write("Created At") + with gui.tag("th"): + gui.write("Created By") + gui.tag("th") + with gui.tag("tbody"): + for api_key in api_keys: + with gui.tag("tr"): + with gui.tag("td"): + gui.write(f"`{api_key.preview}`") + with gui.tag("td"): + gui.write(str(naturaltime(api_key.created_at))) + with gui.tag("td"): + gui.write( + api_key.created_by + and api_key.created_by.full_name() + or "" + ) + with gui.tag("td"): + delete_dialog = gui.use_confirm_dialog( + key=f"delete_api_key_{api_key.id}" + ) + gui.button_with_confirm_dialog( + ref=delete_dialog, + trigger_label=icons.delete, + trigger_type="tertiary", + trigger_className="text-danger p-0 m-0", + modal_title="### Delete API Key", + modal_content=f"Are you sure you want to delete `{api_key.preview}`?\n\n" + "API requests made using this key will be rejected, " + "which could cause any systems still depending on it to break. " + "Once deleted, you'll no longer be able to view or modify this API key.", + confirm_label="Delete", + confirm_className="border-danger bg-danger text-white", + ) + if delete_dialog.pressed_confirm: + api_key.delete() + gui.rerun() def generate_new_api_key(workspace: "Workspace", user: AppUser) -> ApiKey: @@ -54,12 +85,11 @@ def generate_new_api_key(workspace: "Workspace", user: AppUser) -> ApiKey: gui.success( """ -##### API key generated - +**API key generated** Please save this secret key somewhere safe and accessible. For security reasons, **you won't be able to view it again** through your account. If you lose this secret key, you'll need to generate a new one. - """ + """ ) col1, col2 = gui.columns([3, 1], responsive=False) with col1: diff --git a/daras_ai_v2/settings.py b/daras_ai_v2/settings.py index ce78485a1..348ce2b14 100644 --- a/daras_ai_v2/settings.py +++ b/daras_ai_v2/settings.py @@ -73,6 +73,7 @@ "functions", "workspaces", "api_keys", + "managed_secrets", ] MIDDLEWARE = [ @@ -424,6 +425,8 @@ AZURE_SPEECH_ENDPOINT = f"https://{AZURE_SPEECH_REGION}.api.cognitive.microsoft.com" AZURE_TTS_ENDPOINT = f"https://{AZURE_SPEECH_REGION}.tts.speech.microsoft.com" +AZURE_KEY_VAULT_ENDPOINT = config("AZURE_KEY_VAULT_ENDPOINT", "") + AZURE_OPENAI_ENDPOINT_CA = config("AZURE_OPENAI_ENDPOINT_CA", "") AZURE_OPENAI_KEY_CA = config("AZURE_OPENAI_KEY_CA", "") AZURE_OPENAI_ENDPOINT_EASTUS2 = config("AZURE_OPENAI_ENDPOINT_EASTUS2", "") diff --git a/functions/executor.js b/functions/executor.js index 884e29c2b..a6ddfcb7b 100644 --- a/functions/executor.js +++ b/functions/executor.js @@ -8,12 +8,17 @@ Deno.serve(async (req) => { return new Response("Unauthorized", { status: 401 }); } - let { tag, code, variables } = await req.json(); + let { tag, code, variables, env } = await req.json(); let { mockConsole, logs } = captureConsole(tag); let status, response; try { - let retval = isolatedEval(mockConsole, code, variables); + let retval = isolatedEval({ + console: mockConsole, + code, + variables, + process: { env }, + }); if (retval instanceof Function) { retval = retval(variables); } @@ -31,7 +36,7 @@ Deno.serve(async (req) => { return new Response(body, { status }); }); -function isolatedEval(console, code, variables) { +function isolatedEval({ console, code, variables, process }) { // Hide global objects let Deno, global, self, globalThis, window; return eval(code); diff --git a/managed_secrets/__init__.py b/managed_secrets/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/managed_secrets/admin.py b/managed_secrets/admin.py new file mode 100644 index 000000000..a91beaa9f --- /dev/null +++ b/managed_secrets/admin.py @@ -0,0 +1,18 @@ +from django.contrib import admin + +from managed_secrets.models import ManagedSecret + + +@admin.register(ManagedSecret) +class ManagedSecretAdmin(admin.ModelAdmin): + list_display = [ + "__str__", + "workspace", + "created_at", + "updated_at", + "usage_count", + "last_used_at", + ] + autocomplete_fields = ["workspace", "created_by"] + readonly_fields = ["usage_count", "last_used_at", "external_id"] + search_fields = ["name", "external_id"] diff --git a/managed_secrets/apps.py b/managed_secrets/apps.py new file mode 100644 index 000000000..795161c3b --- /dev/null +++ b/managed_secrets/apps.py @@ -0,0 +1,11 @@ +from django.apps import AppConfig + + +class ManagedSecretsConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "managed_secrets" + + def ready(self): + from . import signals + + assert signals diff --git a/managed_secrets/migrations/0001_initial.py b/managed_secrets/migrations/0001_initial.py new file mode 100644 index 000000000..6c54b92ee --- /dev/null +++ b/managed_secrets/migrations/0001_initial.py @@ -0,0 +1,35 @@ +# Generated by Django 5.1.3 on 2024-12-23 15:47 + +import django.db.models.deletion +import uuid +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('app_users', '0023_alter_appusertransaction_workspace'), + ('workspaces', '0006_workspace_description'), + ] + + operations = [ + migrations.CreateModel( + name='ManagedSecret', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('external_id', models.UUIDField(default=uuid.uuid4, editable=False)), + ('name', models.CharField(max_length=255)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('usage_count', models.PositiveIntegerField(db_index=True, default=0)), + ('last_used_at', models.DateTimeField(blank=True, default=None, null=True)), + ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='app_users.appuser')), + ('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='managed_secrets', to='workspaces.workspace')), + ], + options={ + 'unique_together': {('workspace', 'name')}, + }, + ), + ] diff --git a/managed_secrets/migrations/__init__.py b/managed_secrets/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/managed_secrets/models.py b/managed_secrets/models.py new file mode 100644 index 000000000..bf08cd2d6 --- /dev/null +++ b/managed_secrets/models.py @@ -0,0 +1,74 @@ +import uuid + +from django.db import models + +from daras_ai_v2 import settings + +'az role assignment create --role "Key Vault Secrets Officer" --assignee "" --scope "/subscriptions//resourceGroups//providers/Microsoft.KeyVault/vaults/"' + + +class ManagedSecretQuerySet(models.QuerySet): + def create(self, *, value: str, **kwargs): + secret = super().create(**kwargs) + secret.store_value(value) + return secret + + +class ManagedSecret(models.Model): + external_id = models.UUIDField(default=uuid.uuid4, editable=False) + + name = models.CharField(max_length=255) + + workspace = models.ForeignKey( + "workspaces.Workspace", on_delete=models.CASCADE, related_name="managed_secrets" + ) + created_by = models.ForeignKey( + "app_users.AppUser", on_delete=models.SET_NULL, null=True + ) + + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + usage_count = models.PositiveIntegerField(default=0, db_index=True) + last_used_at = models.DateTimeField(null=True, blank=True, default=None) + + objects: ManagedSecretQuerySet = ManagedSecretQuerySet.as_manager() + + value: str | None = None + + class Meta: + unique_together = ("workspace", "name") + + def __str__(self): + return f"{self.name} ({self._external_name()})" + + def store_value(self, value: str): + client = _get_az_secret_client() + client.set_secret(self._external_name(), value) + self.value = value + + def load_value(self): + client = _get_az_secret_client() + self.value = client.get_secret(self._external_name()).value + + def delete_value(self): + import azure.core.exceptions + + client = _get_az_secret_client() + try: + client.begin_delete_secret(self._external_name()) + except azure.core.exceptions.ResourceNotFoundError: + pass + + def _external_name(self): + return f"ms-gooey-{self.external_id}" + + +def _get_az_secret_client(): + from azure.keyvault.secrets import SecretClient + from azure.identity import DefaultAzureCredential + + credential = DefaultAzureCredential() + return SecretClient( + vault_url=settings.AZURE_KEY_VAULT_ENDPOINT, credential=credential + ) diff --git a/managed_secrets/signals.py b/managed_secrets/signals.py new file mode 100644 index 000000000..f34a56dfe --- /dev/null +++ b/managed_secrets/signals.py @@ -0,0 +1,16 @@ +from django.db.models.signals import post_save, post_delete +from django.dispatch import receiver + +from managed_secrets.models import ManagedSecret + + +@receiver(post_save, sender=ManagedSecret) +def on_secret_saved(instance: ManagedSecret, **kwargs): + if instance.value is None: + return + instance.store_value(instance.value) + + +@receiver(post_delete, sender=ManagedSecret) +def on_secret_deleted(instance: ManagedSecret, **kwargs): + instance.delete_value() diff --git a/managed_secrets/tests.py b/managed_secrets/tests.py new file mode 100644 index 000000000..7ce503c2d --- /dev/null +++ b/managed_secrets/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/managed_secrets/views.py b/managed_secrets/views.py new file mode 100644 index 000000000..08592556b --- /dev/null +++ b/managed_secrets/views.py @@ -0,0 +1,257 @@ +import typing + +import gooey_gui as gui +from absl.flags import ValidationError +from django.contrib.humanize.templatetags.humanize import naturaltime +from django.db import IntegrityError + +from daras_ai_v2 import icons +from managed_secrets.models import ManagedSecret + +if typing.TYPE_CHECKING: + from workspaces.models import Workspace + from bots.models import AppUser + + +def manage_secrets_view(workspace: "Workspace", user: "AppUser"): + gui.write( + "Secrets are protected environment variables that allow you to share Gooey Workflows with private keys that are hidden from viewers of the workflow." + ) + + secrets = list(workspace.managed_secrets.order_by("-created_at")) + + table_area = gui.div( + className="table-responsive text-nowrap container-margin-reset" + ) + + create_secret_button_with_dialog( + workspace, + user, + button_label=f"{icons.add} Add Secret", + button_type="secondary", + ) + + with table_area: + with gui.tag("table", className="table table-striped"): + with gui.tag("thead"), gui.tag("tr"): + with gui.tag("th"): + gui.write("Name") + with gui.tag("th"): + gui.write("Value") + with gui.tag("th"): + gui.write("Created At") + with gui.tag("th"): + gui.write("Created By") + gui.tag("th") + with gui.tag("tbody"): + for secret in secrets: + with gui.tag("tr"): + with gui.tag("td"): + gui.write(f"`{secret.name}`") + with gui.tag("td", className="d-flex gap-3"): + if gui.session_state.pop(f"secret:{secret.id}:show", False): + secret.load_value() + if secret.value: + gui.write(f"`{secret.value}`") + else: + gui.write("`" + "*" * 10 + "`") + gui.button( + '', + type="tertiary", + className="m-0 px-1 py-0", + key=f"secret:{secret.id}:show", + ) + with gui.tag("td"): + gui.write(str(naturaltime(secret.created_at))) + with gui.tag("td"): + gui.write( + secret.created_by + and secret.created_by.full_name() + or "" + ) + with gui.tag("td"): + edit_secret_button_with_dialog(secret, workspace, user) + + gui.html('') + + delete_dialog = gui.use_confirm_dialog( + key=f"secrets:{secret.id}:delete" + ) + gui.button_with_confirm_dialog( + ref=delete_dialog, + trigger_label=icons.delete, + trigger_type="tertiary", + trigger_className="text-danger p-0 m-0 ", + modal_title="### Delete Secret", + modal_content=f"Are you sure you want to delete `{secret.name}`?\n\n" + "Functions using this secret may throw errors or silently fail, " + "which could cause any systems still depending on it to break. " + "Once deleted, you'll no longer be able to view or modify this Secret.", + confirm_label="Delete", + confirm_className="border-danger bg-danger text-white", + ) + if delete_dialog.pressed_confirm: + secret.delete() + gui.rerun() + + +def create_secret_button_with_dialog( + workspace: "Workspace", + user: "AppUser", + button_label: str, + button_type: str, + button_className: str = "", +) -> ManagedSecret | None: + dialog = gui.use_confirm_dialog(key="secrets:create", close_on_confirm=False) + + if gui.button( + label=button_label, + type=button_type, + key=dialog.open_btn_key, + className=button_className, + ): + # clear form + gui.session_state.pop("secret:name", None) + gui.session_state.pop("secret:value", None) + dialog.set_open(True) + if not dialog.is_open: + return + + with gui.confirm_dialog( + ref=dialog, + modal_title="### Add Secret Value", + confirm_label="Save", + ): + gui.caption( + "The values of protected variables are secret and enable workflow sharing without revealing API keys." + ) + name = gui.text_input( + label="###### Name", + style=dict(textTransform="uppercase", fontFamily="monospace"), + # language=javascript + onKeyUp="setValue(value.replace(/ /g, '_').replace(/[^a-zA-Z0-9_\$]/g, ''))", + key="secret:name", + ).upper() + if name and name[0].isdigit(): + gui.error( + "Secret name must be a valid JS variable name and cannot start with a number." + ) + value = gui.text_input( + label="###### Value", + key="secret:value", + ) + + if workspace.is_personal: + visible_to = "you" + else: + visible_to = "your workspace members" + gui.caption( + f"Once entered, secret values are encrypted with your login credentials and only visible to {visible_to}." + ) + gui.text_input( + label="###### Workspace", + value=workspace.display_name(user), + disabled=True, + ) + + if dialog.pressed_confirm: + try: + ManagedSecret.objects.create( + workspace=workspace, + created_by=user, + name=name, + value=value, + ) + except ValidationError as e: + gui.error(str(e)) + return + except IntegrityError: + gui.error( + f"Secret with name `{name}` already exists. Please choose a different name." + ) + return + dialog.set_open(False) + gui.rerun() + + +def edit_secret_button_with_dialog( + secret: ManagedSecret, workspace: "Workspace", user: "AppUser" +): + dialog = gui.use_confirm_dialog( + key=f"secret:{secret.id}:edit", close_on_confirm=False + ) + + if gui.button( + '', + type="link", + className="p-0 m-0", + key=dialog.open_btn_key, + ): + gui.session_state.pop("secret:name", None) + gui.session_state.pop("secret:value", None) + dialog.set_open(True) + if not dialog.is_open: + return + + header, body, footer = gui.modal_scaffold() + with header: + if secret: + gui.write( + f'### Edit {secret.name}', + unsafe_allow_html=True, + ) + else: + gui.write("### Add Secret Value") + with body: + gui.caption( + "The values of protected variables are secret and enable workflow sharing without revealing API keys." + ) + + name = gui.text_input( + label="###### Name", + style=dict(textTransform="uppercase", fontFamily="monospace"), + # language=javascript + onKeyUp="setValue(value.replace(/ /g, '_').replace(/[^a-zA-Z0-9_\$]/g, ''))", + key="secret:name", + value=secret.name, + ) + if name and name[0].isdigit(): + gui.error( + "Secret name must be a valid JS variable name and cannot start with a number." + ) + value = gui.text_input( + label="###### New Value", + key="secret:value", + ) + + if workspace.is_personal: + visible_to = "you" + else: + visible_to = "your workspace members" + gui.caption( + f"Once entered, secret values are encrypted with your login credentials and only visible to {visible_to}." + ) + gui.text_input( + label="###### Workspace", + value=workspace.display_name(user), + disabled=True, + ) + + with footer: + gui.button( + label="Cancel", + key=dialog.close_btn_key, + type="tertiary", + ) + gui.button( + label="Save", + key=dialog.confirm_btn_key, + type="primary", + ) + + if dialog.pressed_confirm: + secret.name = name + secret.value = value + secret.save() + dialog.set_open(False) + gui.rerun() diff --git a/poetry.lock b/poetry.lock index c5b2cf268..4b568e519 100644 --- a/poetry.lock +++ b/poetry.lock @@ -350,6 +350,59 @@ files = [ {file = "azure_cognitiveservices_speech-1.37.0-py3-none-win_amd64.whl", hash = "sha256:a18fb45490cdcd681407ccaad9560fb14dda1c8276de297e219ea1a880467e28"}, ] +[[package]] +name = "azure-core" +version = "1.32.0" +description = "Microsoft Azure Core Library for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "azure_core-1.32.0-py3-none-any.whl", hash = "sha256:eac191a0efb23bfa83fddf321b27b122b4ec847befa3091fa736a5c32c50d7b4"}, + {file = "azure_core-1.32.0.tar.gz", hash = "sha256:22b3c35d6b2dae14990f6c1be2912bf23ffe50b220e708a28ab1bb92b1c730e5"}, +] + +[package.dependencies] +requests = ">=2.21.0" +six = ">=1.11.0" +typing-extensions = ">=4.6.0" + +[package.extras] +aio = ["aiohttp (>=3.0)"] + +[[package]] +name = "azure-identity" +version = "1.19.0" +description = "Microsoft Azure Identity Library for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "azure_identity-1.19.0-py3-none-any.whl", hash = "sha256:e3f6558c181692d7509f09de10cca527c7dce426776454fb97df512a46527e81"}, + {file = "azure_identity-1.19.0.tar.gz", hash = "sha256:500144dc18197d7019b81501165d4fa92225f03778f17d7ca8a2a180129a9c83"}, +] + +[package.dependencies] +azure-core = ">=1.31.0" +cryptography = ">=2.5" +msal = ">=1.30.0" +msal-extensions = ">=1.2.0" +typing-extensions = ">=4.0.0" + +[[package]] +name = "azure-keyvault-secrets" +version = "4.9.0" +description = "Microsoft Azure Key Vault Secrets Client Library for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "azure_keyvault_secrets-4.9.0-py3-none-any.whl", hash = "sha256:33c7e2aca2cc2092cebc8c6e96eca36a5cc30c767e16ea429c5fa21270e9fba6"}, + {file = "azure_keyvault_secrets-4.9.0.tar.gz", hash = "sha256:2a03bb2ffd9a0d6c8ad1c330d9d0310113985a9de06607ece378fd72a5889fe1"}, +] + +[package.dependencies] +azure-core = ">=1.31.0" +isodate = ">=0.6.1" +typing-extensions = ">=4.0.1" + [[package]] name = "beautifulsoup4" version = "4.12.2" @@ -2467,6 +2520,17 @@ qtconsole = ["qtconsole"] test = ["pickleshare", "pytest (<7.1)", "pytest-asyncio (<0.22)", "testpath"] test-extra = ["curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.22)", "pandas", "pickleshare", "pytest (<7.1)", "pytest-asyncio (<0.22)", "testpath", "trio"] +[[package]] +name = "isodate" +version = "0.7.2" +description = "An ISO 8601 date/time/duration parser and formatter" +optional = false +python-versions = ">=3.7" +files = [ + {file = "isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15"}, + {file = "isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6"}, +] + [[package]] name = "itsdangerous" version = "2.1.2" @@ -3178,16 +3242,6 @@ files = [ {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, @@ -3375,6 +3429,40 @@ numpy = [ [package.extras] dev = ["absl-py", "pyink", "pylint (>=2.6.0)", "pytest", "pytest-xdist"] +[[package]] +name = "msal" +version = "1.31.1" +description = "The Microsoft Authentication Library (MSAL) for Python library enables your app to access the Microsoft Cloud by supporting authentication of users with Microsoft Azure Active Directory accounts (AAD) and Microsoft Accounts (MSA) using industry standard OAuth2 and OpenID Connect." +optional = false +python-versions = ">=3.7" +files = [ + {file = "msal-1.31.1-py3-none-any.whl", hash = "sha256:29d9882de247e96db01386496d59f29035e5e841bcac892e6d7bf4390bf6bd17"}, + {file = "msal-1.31.1.tar.gz", hash = "sha256:11b5e6a3f802ffd3a72107203e20c4eac6ef53401961b880af2835b723d80578"}, +] + +[package.dependencies] +cryptography = ">=2.5,<46" +PyJWT = {version = ">=1.0.0,<3", extras = ["crypto"]} +requests = ">=2.0.0,<3" + +[package.extras] +broker = ["pymsalruntime (>=0.14,<0.18)", "pymsalruntime (>=0.17,<0.18)"] + +[[package]] +name = "msal-extensions" +version = "1.2.0" +description = "Microsoft Authentication Library extensions (MSAL EX) provides a persistence API that can save your data on disk, encrypted on Windows, macOS and Linux. Concurrent data access will be coordinated by a file lock mechanism." +optional = false +python-versions = ">=3.7" +files = [ + {file = "msal_extensions-1.2.0-py3-none-any.whl", hash = "sha256:cf5ba83a2113fa6dc011a254a72f1c223c88d7dfad74cc30617c4679a417704d"}, + {file = "msal_extensions-1.2.0.tar.gz", hash = "sha256:6f41b320bfd2933d631a215c91ca0dd3e67d84bd1a2f50ce917d5874ec646bef"}, +] + +[package.dependencies] +msal = ">=1.29,<2" +portalocker = ">=1.4,<3" + [[package]] name = "msgpack" version = "1.0.7" @@ -4064,6 +4152,25 @@ files = [ dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] +[[package]] +name = "portalocker" +version = "2.10.1" +description = "Wraps the portalocker recipe for easy usage" +optional = false +python-versions = ">=3.8" +files = [ + {file = "portalocker-2.10.1-py3-none-any.whl", hash = "sha256:53a5984ebc86a025552264b459b46a2086e269b21823cb572f8f28ee759e45bf"}, + {file = "portalocker-2.10.1.tar.gz", hash = "sha256:ef1bf844e878ab08aee7e40184156e1151f228f103aa5c6bd0724cc330960f8f"}, +] + +[package.dependencies] +pywin32 = {version = ">=226", markers = "platform_system == \"Windows\""} + +[package.extras] +docs = ["sphinx (>=1.7.1)"] +redis = ["redis"] +tests = ["pytest (>=5.4.1)", "pytest-cov (>=2.8.1)", "pytest-mypy (>=0.8.0)", "pytest-timeout (>=2.1.0)", "redis", "sphinx (>=6.0.0)", "types-redis"] + [[package]] name = "pre-commit" version = "3.5.0" @@ -4807,7 +4914,6 @@ files = [ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, - {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, @@ -4815,15 +4921,8 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, - {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, - {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, - {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, - {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, @@ -4840,7 +4939,6 @@ files = [ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, - {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, @@ -4848,7 +4946,6 @@ files = [ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, - {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, @@ -6865,4 +6962,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = ">=3.10,<3.13" -content-hash = "17078d4361855123b09b44db04274faea5d4760768f10329476a4ff96f2f84a0" +content-hash = "cb52f5ed2c0ed82485a29ee3d341a87ab63d7693762901a7b09fc1959e008f69" diff --git a/pyproject.toml b/pyproject.toml index 7e611f552..ac7276f45 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -88,6 +88,8 @@ django-safedelete = "^1.4.0" numexpr = "^2.10.1" django-csp = "^3.8" python-pptx = "^1.0.2" +azure-identity = "^1.19.0" +azure-keyvault-secrets = "^4.9.0" [tool.poetry.group.dev.dependencies] watchdog = "^2.1.9" diff --git a/recipes/Functions.py b/recipes/Functions.py index 8472ddade..637b4e286 100644 --- a/recipes/Functions.py +++ b/recipes/Functions.py @@ -5,12 +5,15 @@ from pydantic import BaseModel, Field from bots.models import Workflow -from daras_ai_v2 import settings +from daras_ai_v2 import settings, icons from daras_ai_v2.base import BasePage -from daras_ai_v2.exceptions import raise_for_status -from daras_ai_v2.field_render import field_title +from daras_ai_v2.exceptions import raise_for_status, UserError +from daras_ai_v2.field_render import field_title, field_desc +from daras_ai_v2.functional import map_parallel from daras_ai_v2.variables_widget import variables_input from functions.models import CalledFunction, VariableSchema +from managed_secrets.models import ManagedSecret +from managed_secrets.views import create_secret_button_with_dialog class ConsoleLogs(BaseModel): @@ -26,7 +29,7 @@ class FunctionsPage(BasePage): price = 1 class RequestModel(BaseModel): - code: str = Field( + code: str | None = Field( None, title="Code", description="The JS code to be executed.", @@ -41,6 +44,13 @@ class RequestModel(BaseModel): title="⌥ Variables Schema", description="Schema for variables to be used in the variables input", ) + secrets: list[str] | None = Field( + None, + title="Secrets", + description="Secrets enable workflow sharing without revealing sensitive environment variables like API keys.\n" + "Use them in your functions from nodejs standard `process.env.SECRET_NAME`\n\n" + "Manage your secrets in the [account keys](/account/api-keys/) section.", + ) class ResponseModel(BaseModel): return_value: typing.Any = Field( @@ -67,12 +77,24 @@ def run_v2( sr = self.current_sr tag = f"run_id={sr.run_id}&uid={sr.uid}" + if request.secrets: + yield "Decrypting secrets..." + secret_values = map_parallel(self._load_secret, request.secrets) + env = dict(zip(request.secrets, secret_values)) + else: + env = None + yield "Running your code..." # this will run functions/executor.js in deno deploy r = requests.post( settings.DENO_FUNCTIONS_URL, headers={"Authorization": f"Basic {settings.DENO_FUNCTIONS_AUTH_TOKEN}"}, - json=dict(code=request.code, variables=request.variables or {}, tag=tag), + json=dict( + code=request.code, + variables=request.variables or {}, + tag=tag, + env=env, + ), ) raise_for_status(r) data = r.json() @@ -80,6 +102,18 @@ def run_v2( response.return_value = data.get("retval") response.error = data.get("error") + def _load_secret(self, name: str) -> str: + try: + secret = ManagedSecret.objects.get( + workspace=self.current_workspace, name=name + ) + except ManagedSecret.DoesNotExist: + raise UserError( + f"Secret `{name}` not found. Please go to your [account keys](/account/api-keys/) section and provide this value." + ) + secret.load_value() + return secret.value + def render_form_v2(self): gui.code_editor( label="##### " + field_title(self.RequestModel, "code"), @@ -103,6 +137,36 @@ def render_variables(self): description="Pass custom parameters to your function and access the parent workflow data. " "Variables will be passed down as the first argument to your anonymous JS function.", ) + with gui.div(className="d-flex align-items-center gap-3 mb-2"): + gui.markdown( + "###### " + + ' ' + + field_title(self.RequestModel, "secrets"), + help=field_desc(self.RequestModel, "secrets"), + unsafe_allow_html=True, + ) + create_secret_button_with_dialog( + self.current_workspace, + self.request.user, + button_label=f"{icons.add} Add", + button_type="tertiary", + button_className="p-1 mb-2", + ) + + options = list( + set( + self.current_workspace.managed_secrets.order_by( + "-created_at" + ).values_list("name", flat=True) + ) + | set(gui.session_state.get("secrets", [])) + ) + gui.multiselect( + label="", + options=options, + key="secrets", + allow_none=True, + ) def render_output(self): if error := gui.session_state.get("error"): diff --git a/routers/account.py b/routers/account.py index 598ada290..87a12a809 100644 --- a/routers/account.py +++ b/routers/account.py @@ -19,6 +19,7 @@ from daras_ai_v2.meta_content import raw_build_meta_tags from daras_ai_v2.profiles import edit_user_profile_page from daras_ai_v2.urls import paginate_queryset, paginate_button +from managed_secrets.views import manage_secrets_view from payments.webhooks import PaypalWebhookHandler from routers.custom_api_router import CustomAPIRouter from routers.root import explore_page, page_wrapper, get_og_url_path @@ -353,10 +354,14 @@ def _render_run(pr: PublishedRun): def api_keys_tab(request: Request): - gui.write("# 🔐 API Keys") workspace = get_current_workspace(request.user, request.session) + + gui.write("## 🔐 API Keys") manage_api_keys(workspace=workspace, user=request.user) + gui.write("## 🛡 Secrets") + manage_secrets_view(workspace, request.user) + @contextmanager def account_page_wrapper(request: Request, current_tab: TabData): diff --git a/usage_costs/migrations/0023_alter_modelpricing_model_name.py b/usage_costs/migrations/0023_alter_modelpricing_model_name.py new file mode 100644 index 000000000..449958bec --- /dev/null +++ b/usage_costs/migrations/0023_alter_modelpricing_model_name.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.3 on 2024-12-17 09:42 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('usage_costs', '0022_alter_modelpricing_model_name'), + ] + + operations = [ + migrations.AlterField( + model_name='modelpricing', + name='model_name', + field=models.CharField(choices=[('o1_preview', 'o1-preview (openai)'), ('o1_mini', 'o1-mini (openai)'), ('gpt_4_o', 'GPT-4o (openai)'), ('gpt_4_o_mini', 'GPT-4o-mini (openai)'), ('chatgpt_4_o', 'ChatGPT-4o (openai) 🧪'), ('gpt_4_turbo_vision', 'GPT-4 Turbo with Vision (openai)'), ('gpt_4_vision', 'GPT-4 Vision (openai) 🔻'), ('gpt_4_turbo', 'GPT-4 Turbo (openai)'), ('gpt_4', 'GPT-4 (openai)'), ('gpt_4_32k', 'GPT-4 32K (openai) 🔻'), ('gpt_3_5_turbo', 'ChatGPT (openai)'), ('gpt_3_5_turbo_16k', 'ChatGPT 16k (openai)'), ('gpt_3_5_turbo_instruct', 'GPT-3.5 Instruct (openai) 🔻'), ('llama3_3_70b', 'Llama 3.3 70B'), ('llama3_2_90b_vision', 'Llama 3.2 90B + Vision (Meta AI)'), ('llama3_2_11b_vision', 'Llama 3.2 11B + Vision (Meta AI)'), ('llama3_2_3b', 'Llama 3.2 3B (Meta AI)'), ('llama3_2_1b', 'Llama 3.2 1B (Meta AI)'), ('llama3_1_70b', 'Llama 3.1 70B (Meta AI)'), ('llama3_1_8b', 'Llama 3.1 8B (Meta AI)'), ('llama3_70b', 'Llama 3 70B (Meta AI)'), ('llama3_8b', 'Llama 3 8B (Meta AI)'), ('mixtral_8x7b_instruct_0_1', 'Mixtral 8x7b Instruct v0.1 (Mistral)'), ('gemma_2_9b_it', 'Gemma 2 9B (Google)'), ('gemma_7b_it', 'Gemma 7B (Google)'), ('gemini_1_5_flash', 'Gemini 1.5 Flash (Google)'), ('gemini_1_5_pro', 'Gemini 1.5 Pro (Google)'), ('gemini_1_pro_vision', 'Gemini 1.0 Pro Vision (Google)'), ('gemini_1_pro', 'Gemini 1.0 Pro (Google)'), ('palm2_chat', 'PaLM 2 Chat (Google)'), ('palm2_text', 'PaLM 2 Text (Google)'), ('claude_3_5_sonnet', 'Claude 3.5 Sonnet (Anthropic)'), ('claude_3_opus', 'Claude 3 Opus [L] (Anthropic)'), ('claude_3_sonnet', 'Claude 3 Sonnet [M] (Anthropic)'), ('claude_3_haiku', 'Claude 3 Haiku [S] (Anthropic)'), ('afrollama_v1', 'AfroLlama3 v1 (Jacaranda)'), ('llama3_8b_cpt_sea_lion_v2_1_instruct', 'Llama3 8B CPT SEA-LIONv2.1 Instruct (aisingapore)'), ('sarvam_2b', 'Sarvam 2B (sarvamai)'), ('llama_3_groq_70b_tool_use', 'Llama 3 Groq 70b Tool Use [Deprecated]'), ('llama_3_groq_8b_tool_use', 'Llama 3 Groq 8b Tool Use [Deprecated]'), ('llama2_70b_chat', 'Llama 2 70B Chat [Deprecated] (Meta AI)'), ('sea_lion_7b_instruct', 'SEA-LION-7B-Instruct [Deprecated] (aisingapore)'), ('llama3_8b_cpt_sea_lion_v2_instruct', 'Llama3 8B CPT SEA-LIONv2 Instruct [Deprecated] (aisingapore)'), ('text_davinci_003', 'GPT-3.5 Davinci-3 [Deprecated] (openai)'), ('text_davinci_002', 'GPT-3.5 Davinci-2 [Deprecated] (openai)'), ('code_davinci_002', 'Codex [Deprecated] (openai)'), ('text_curie_001', 'Curie [Deprecated] (openai)'), ('text_babbage_001', 'Babbage [Deprecated] (openai)'), ('text_ada_001', 'Ada [Deprecated] (openai)'), ('protogen_2_2', 'Protogen V2.2 (darkstorm2150)'), ('epicdream', 'epiCDream (epinikion)'), ('flux_1_dev', 'FLUX.1 [dev]'), ('dream_shaper', 'DreamShaper (Lykon)'), ('dreamlike_2', 'Dreamlike Photoreal 2.0 (dreamlike.art)'), ('sd_2', 'Stable Diffusion v2.1 (stability.ai)'), ('sd_1_5', 'Stable Diffusion v1.5 (RunwayML)'), ('dall_e', 'DALL·E 2 (OpenAI)'), ('dall_e_3', 'DALL·E 3 (OpenAI)'), ('openjourney_2', 'Open Journey v2 beta (PromptHero)'), ('openjourney', 'Open Journey (PromptHero)'), ('analog_diffusion', 'Analog Diffusion (wavymulder)'), ('protogen_5_3', 'Protogen v5.3 (darkstorm2150)'), ('jack_qiao', 'Stable Diffusion v1.4 [Deprecated] (Jack Qiao)'), ('rodent_diffusion_1_5', 'Rodent Diffusion 1.5 [Deprecated] (NerdyRodent)'), ('deepfloyd_if', 'DeepFloyd IF [Deprecated] (stability.ai)'), ('dream_shaper', 'DreamShaper (Lykon)'), ('dreamlike_2', 'Dreamlike Photoreal 2.0 (dreamlike.art)'), ('sd_2', 'Stable Diffusion v2.1 (stability.ai)'), ('sd_1_5', 'Stable Diffusion v1.5 (RunwayML)'), ('dall_e', 'Dall-E (OpenAI)'), ('instruct_pix2pix', '✨ InstructPix2Pix (Tim Brooks)'), ('openjourney_2', 'Open Journey v2 beta (PromptHero) 🐢'), ('openjourney', 'Open Journey (PromptHero) 🐢'), ('analog_diffusion', 'Analog Diffusion (wavymulder) 🐢'), ('protogen_5_3', 'Protogen v5.3 (darkstorm2150) 🐢'), ('jack_qiao', 'Stable Diffusion v1.4 [Deprecated] (Jack Qiao)'), ('rodent_diffusion_1_5', 'Rodent Diffusion 1.5 [Deprecated] (NerdyRodent)'), ('sd_2', 'Stable Diffusion v2.1 (stability.ai)'), ('runway_ml', 'Stable Diffusion v1.5 (RunwayML)'), ('dall_e', 'Dall-E (OpenAI)'), ('jack_qiao', 'Stable Diffusion v1.4 [Deprecated] (Jack Qiao)'), ('wav2lip', 'LipSync (wav2lip)'), ('sadtalker', 'LipSync (sadtalker)')], help_text='The name of the model. Only used for Display purposes.', max_length=255), + ), + ]