[GH-ISSUE #12899] issue: is_global flag not preserved on function export (imported from https://openwebui.com/f/owndev/azure_ai) #16748

Closed
opened 2026-04-19 22:36:18 -05:00 by GiteaMirror · 3 comments
Owner

Originally created by @EvKoh on GitHub (Apr 15, 2025).
Original GitHub issue: https://github.com/open-webui/open-webui/issues/12899

Check Existing Issues

  • I have searched the existing issues and discussions.
  • I am using the latest version of Open WebUI.

Installation Method

Git Clone

Open WebUI Version

v0.6.5

Ollama Version (if applicable)

No response

Operating System

ubuntu, docker -> ghcr.io/open-webui/open-webui:latest

Browser (if applicable)

Version 135.0.7049.84 (Build officiel) snap (64 bits)

Confirmation

  • I have read and followed all instructions in README.md.
  • I am using the latest version of both Open WebUI and Ollama.
  • I have included the browser console logs.
  • I have included the Docker container logs.
  • I have listed steps to reproduce the bug in detail.

Expected Behavior

  • When importing a function with "is_global": true defined in its manifest, the exported JSON and internal configuration should preserve this flag as true.
  • The function should remain globally available as specified in its manifest.

Actual Behavior

  • After importing the function from https://openwebui.com/f/owndev/azure_ai with "is_global": true, exporting the function later returns a JSON where "is_global": false.
  • This reset of the flag contradicts the initial configuration, preventing the function from remaining global.

Steps to Reproduce

  1. Import the function from https://openwebui.com/f/owndev/azure_ai into your OpenWebUI instance, ensuring that the manifest contains "is_global": true.
  2. Verify in the administration interface that the function appears as global (if applicable).
  3. Export the function from the administration interface.
  4. Open the exported JSON file and observe that the is_global field has been reset to false.

Logs & Screenshots

Image

This is a raw export of my function just before importing it. It clearly shows that is_global is correctly present with the value true in the initial import, but after exporting again from the interface, is_global is missing or automatically overwritten to false :

[
  {
    "id": "azure_ai_global",
    "user_id": "c788549a-0cc0-4db3-9fe0-8d2af6b4c155",
    "name": "Azure AI Global",
    "type": "pipe",
    "content": "\"\"\"\ntitle: Azure AI Foundry Pipeline\nauthor: owndev\nauthor_url: https://github.com/owndev\nproject_url: https://github.com/owndev/Open-WebUI-Functions\nfunding_url: https://github.com/owndev/Open-WebUI-Functions\nversion: 2.1.0\nlicense: Apache License 2.0\ndescription: A pipeline for interacting with Azure AI services, enabling seamless communication with various AI models via configurable headers and robust error handling. This includes support for Azure OpenAI models as well as other Azure AI models by dynamically managing headers and request configurations.\nfeatures:\n  - Supports dynamic model specification via headers.\n  - Filters valid parameters to ensure clean requests.\n  - Handles streaming and non-streaming responses.\n  - Provides flexible timeout and error handling mechanisms.\n  - Compatible with Azure OpenAI and other Azure AI models.\n  - Predefined models for easy access.\n  - Encrypted storage of sensitive API keys\n\"\"\"\n\nfrom typing import List, Union, Generator, Iterator, Optional, Dict, Any\nfrom fastapi.responses import StreamingResponse\nfrom pydantic import BaseModel, Field, GetCoreSchemaHandler\nfrom starlette.background import BackgroundTask\nfrom open_webui.env import AIOHTTP_CLIENT_TIMEOUT, SRC_LOG_LEVELS\nfrom cryptography.fernet import Fernet, InvalidToken\nimport aiohttp\nimport json\nimport os\nimport logging\nimport base64\nimport hashlib\nfrom pydantic_core import core_schema\n\n\n# Simplified encryption implementation with automatic handling\nclass EncryptedStr(str):\n    \"\"\"A string type that automatically handles encryption/decryption\"\"\"\n\n    @classmethod\n    def _get_encryption_key(cls) -> Optional[bytes]:\n        \"\"\"\n        Generate encryption key from WEBUI_SECRET_KEY if available\n        Returns None if no key is configured\n        \"\"\"\n        secret = os.getenv(\"WEBUI_SECRET_KEY\")\n        if not secret:\n            return None\n\n        hashed_key = hashlib.sha256(secret.encode()).digest()\n        return base64.urlsafe_b64encode(hashed_key)\n\n    @classmethod\n    def encrypt(cls, value: str) -> str:\n        \"\"\"\n        Encrypt a string value if a key is available\n        Returns the original value if no key is available\n        \"\"\"\n        if not value or value.startswith(\"encrypted:\"):\n            return value\n\n        key = cls._get_encryption_key()\n        if not key:  # No encryption if no key\n            return value\n\n        f = Fernet(key)\n        encrypted = f.encrypt(value.encode())\n        return f\"encrypted:{encrypted.decode()}\"\n\n    @classmethod\n    def decrypt(cls, value: str) -> str:\n        \"\"\"\n        Decrypt an encrypted string value if a key is available\n        Returns the original value if no key is available or decryption fails\n        \"\"\"\n        if not value or not value.startswith(\"encrypted:\"):\n            return value\n\n        key = cls._get_encryption_key()\n        if not key:  # No decryption if no key\n            return value[len(\"encrypted:\") :]  # Return without prefix\n\n        try:\n            encrypted_part = value[len(\"encrypted:\") :]\n            f = Fernet(key)\n            decrypted = f.decrypt(encrypted_part.encode())\n            return decrypted.decode()\n        except (InvalidToken, Exception):\n            return value\n\n    # Pydantic integration\n    @classmethod\n    def __get_pydantic_core_schema__(\n        cls, _source_type: Any, _handler: GetCoreSchemaHandler\n    ) -> core_schema.CoreSchema:\n        return core_schema.union_schema(\n            [\n                core_schema.is_instance_schema(cls),\n                core_schema.chain_schema(\n                    [\n                        core_schema.str_schema(),\n                        core_schema.no_info_plain_validator_function(\n                            lambda value: cls(cls.encrypt(value) if value else value)\n                        ),\n                    ]\n                ),\n            ],\n            serialization=core_schema.plain_serializer_function_ser_schema(\n                lambda instance: str(instance)\n            ),\n        )\n\n    def get_decrypted(self) -> str:\n        \"\"\"Get the decrypted value\"\"\"\n        return self.decrypt(self)\n\n\n# Helper functions\nasync def cleanup_response(\n    response: Optional[aiohttp.ClientResponse],\n    session: Optional[aiohttp.ClientSession],\n) -> None:\n    \"\"\"\n    Clean up the response and session objects.\n\n    Args:\n        response: The ClientResponse object to close\n        session: The ClientSession object to close\n    \"\"\"\n    if response:\n        response.close()\n    if session:\n        await session.close()\n\n\nclass Pipe:\n    # Environment variables for API key, endpoint, and optional model\n    class Valves(BaseModel):\n        # API key for Azure AI\n        AZURE_AI_API_KEY: EncryptedStr = Field(\n            default=os.getenv(\"AZURE_AI_API_KEY\", \"API_KEY\"),\n            description=\"API key for Azure AI\",\n        )\n\n        # Endpoint for Azure AI (e.g. \"https://<your-endpoint>/chat/completions?api-version=2024-05-01-preview\" or \"https://<your-endpoint>/openai/deployments/gpt-4o/chat/completions?api-version=2024-08-01-preview\")\n        AZURE_AI_ENDPOINT: str = Field(\n            default=os.getenv(\n                \"AZURE_AI_ENDPOINT\",\n                \"https://<your-endpoint>/chat/completions?api-version=2024-05-01-preview\",\n            ),\n            description=\"Endpoint for Azure AI\",\n        )\n\n        # Optional model name, only necessary if not Azure OpenAI or if model name not in URL (e.g. \"https://<your-endpoint>/openai/deployments/<model-name>/chat/completions\")\n        AZURE_AI_MODEL: str = Field(\n            default=os.getenv(\"AZURE_AI_MODEL\", \"\"),\n            description=\"Optional model name for Azure AI\",\n        )\n\n        # Switch for sending model name in request body\n        AZURE_AI_MODEL_IN_BODY: bool = Field(\n            default=os.getenv(\"AZURE_AI_MODEL_IN_BODY\", False),\n            description=\"If True, include the model name in the request body instead of as a header.\",\n        )\n\n        # Flag to indicate if predefined Azure AI models should be used\n        USE_PREDEFINED_AZURE_AI_MODELS: bool = Field(\n            default=os.getenv(\"USE_PREDEFINED_AZURE_AI_MODELS\", False),\n            description=\"Flag to indicate if predefined Azure AI models should be used.\",\n        )\n\n        # If True, use Authorization header with Bearer token instead of api-key header.\n        USE_AUTHORIZATION_HEADER: bool = Field(\n            default=bool(os.getenv(\"AZURE_AI_USE_AUTHORIZATION_HEADER\", False)),\n            description=\"Set to True to use Authorization header with Bearer token instead of api-key header.\",\n        )\n\n    def __init__(self):\n        self.valves = self.Valves()\n        self.name: str = \"Azure AI\"\n\n    def validate_environment(self) -> None:\n        \"\"\"\n        Validates that required environment variables are set.\n\n        Raises:\n            ValueError: If required environment variables are not set.\n        \"\"\"\n        # Access the decrypted API key\n        api_key = self.valves.AZURE_AI_API_KEY.get_decrypted()\n        if not api_key:\n            raise ValueError(\"AZURE_AI_API_KEY is not set!\")\n        if not self.valves.AZURE_AI_ENDPOINT:\n            raise ValueError(\"AZURE_AI_ENDPOINT is not set!\")\n\n    def get_headers(self) -> Dict[str, str]:\n        \"\"\"\n        Constructs the headers for the API request, including the model name if defined.\n\n        Returns:\n            Dictionary containing the required headers for the API request.\n        \"\"\"\n        # Access the decrypted API key\n        api_key = self.valves.AZURE_AI_API_KEY.get_decrypted()\n        if self.valves.USE_AUTHORIZATION_HEADER:\n            headers = {\n                \"Authorization\": f\"Bearer {api_key}\",\n                \"Content-Type\": \"application/json\",\n            }\n        else:\n            headers = {\"api-key\": api_key, \"Content-Type\": \"application/json\"}\n\n        # If the valve indicates that the model name should be in the body,\n        # add it to the filtered body.\n        if self.valves.AZURE_AI_MODEL and not self.valves.AZURE_AI_MODEL_IN_BODY:\n            headers[\"x-ms-model-mesh-model-name\"] = self.valves.AZURE_AI_MODEL\n        return headers\n\n    def validate_body(self, body: Dict[str, Any]) -> None:\n        \"\"\"\n        Validates the request body to ensure required fields are present.\n\n        Args:\n            body: The request body to validate\n\n        Raises:\n            ValueError: If required fields are missing or invalid.\n        \"\"\"\n        if \"messages\" not in body or not isinstance(body[\"messages\"], list):\n            raise ValueError(\"The 'messages' field is required and must be a list.\")\n\n    def get_azure_models(self) -> List[Dict[str, str]]:\n        \"\"\"\n        Returns a list of predefined Azure AI models.\n\n        Returns:\n            List of dictionaries containing model id and name.\n        \"\"\"\n        return [\n            {\"id\": \"AI21-Jamba-1.5-Large\", \"name\": \"AI21 Jamba 1.5 Large\"},\n            {\"id\": \"AI21-Jamba-1.5-Mini\", \"name\": \"AI21 Jamba 1.5 Mini\"},\n            {\"id\": \"Codestral-2501\", \"name\": \"Codestral 25.01\"},\n            {\"id\": \"Cohere-command-r\", \"name\": \"Cohere Command R\"},\n            {\"id\": \"Cohere-command-r-08-2024\", \"name\": \"Cohere Command R 08-2024\"},\n            {\"id\": \"Cohere-command-r-plus\", \"name\": \"Cohere Command R+\"},\n            {\n                \"id\": \"Cohere-command-r-plus-08-2024\",\n                \"name\": \"Cohere Command R+ 08-2024\",\n            },\n            {\"id\": \"DeepSeek-R1\", \"name\": \"DeepSeek-R1\"},\n            {\"id\": \"DeepSeek-V3\", \"name\": \"DeepSeek-V3\"},\n            {\"id\": \"jais-30b-chat\", \"name\": \"JAIS 30b Chat\"},\n            {\n                \"id\": \"Llama-3.2-11B-Vision-Instruct\",\n                \"name\": \"Llama-3.2-11B-Vision-Instruct\",\n            },\n            {\n                \"id\": \"Llama-3.2-90B-Vision-Instruct\",\n                \"name\": \"Llama-3.2-90B-Vision-Instruct\",\n            },\n            {\"id\": \"Llama-3.3-70B-Instruct\", \"name\": \"Llama-3.3-70B-Instruct\"},\n            {\"id\": \"Meta-Llama-3-70B-Instruct\", \"name\": \"Meta-Llama-3-70B-Instruct\"},\n            {\"id\": \"Meta-Llama-3-8B-Instruct\", \"name\": \"Meta-Llama-3-8B-Instruct\"},\n            {\n                \"id\": \"Meta-Llama-3.1-405B-Instruct\",\n                \"name\": \"Meta-Llama-3.1-405B-Instruct\",\n            },\n            {\n                \"id\": \"Meta-Llama-3.1-70B-Instruct\",\n                \"name\": \"Meta-Llama-3.1-70B-Instruct\",\n            },\n            {\"id\": \"Meta-Llama-3.1-8B-Instruct\", \"name\": \"Meta-Llama-3.1-8B-Instruct\"},\n            {\"id\": \"Ministral-3B\", \"name\": \"Ministral 3B\"},\n            {\"id\": \"Mistral-large\", \"name\": \"Mistral Large\"},\n            {\"id\": \"Mistral-large-2407\", \"name\": \"Mistral Large (2407)\"},\n            {\"id\": \"Mistral-Large-2411\", \"name\": \"Mistral Large 24.11\"},\n            {\"id\": \"Mistral-Nemo\", \"name\": \"Mistral Nemo\"},\n            {\"id\": \"Mistral-small\", \"name\": \"Mistral Small\"},\n            {\"id\": \"mistral-small-2503\", \"name\": \"Mistral Small 3.1\"},\n            {\"id\": \"gpt-4o\", \"name\": \"OpenAI GPT-4o\"},\n            {\"id\": \"gpt-4o-mini\", \"name\": \"OpenAI GPT-4o mini\"},\n            {\"id\": \"o1\", \"name\": \"OpenAI o1\"},\n            {\"id\": \"o1-mini\", \"name\": \"OpenAI o1-mini\"},\n            {\"id\": \"o1-preview\", \"name\": \"OpenAI o1-preview\"},\n            {\"id\": \"o3-mini\", \"name\": \"OpenAI o3-mini\"},\n            {\n                \"id\": \"Phi-3-medium-128k-instruct\",\n                \"name\": \"Phi-3-medium instruct (128k)\",\n            },\n            {\"id\": \"Phi-3-medium-4k-instruct\", \"name\": \"Phi-3-medium instruct (4k)\"},\n            {\"id\": \"Phi-3-mini-128k-instruct\", \"name\": \"Phi-3-mini instruct (128k)\"},\n            {\"id\": \"Phi-3-mini-4k-instruct\", \"name\": \"Phi-3-mini instruct (4k)\"},\n            {\"id\": \"Phi-3-small-128k-instruct\", \"name\": \"Phi-3-small instruct (128k)\"},\n            {\"id\": \"Phi-3-small-8k-instruct\", \"name\": \"Phi-3-small instruct (8k)\"},\n            {\"id\": \"Phi-3.5-mini-instruct\", \"name\": \"Phi-3.5-mini instruct (128k)\"},\n            {\"id\": \"Phi-3.5-MoE-instruct\", \"name\": \"Phi-3.5-MoE instruct (128k)\"},\n            {\"id\": \"Phi-3.5-vision-instruct\", \"name\": \"Phi-3.5-vision instruct (128k)\"},\n            {\"id\": \"Phi-4\", \"name\": \"Phi-4\"},\n            {\"id\": \"Phi-4-mini-instruct\", \"name\": \"Phi-4 mini instruct\"},\n            {\"id\": \"Phi-4-multimodal-instruct\", \"name\": \"Phi-4 multimodal instruct\"},\n        ]\n\n    def pipes(self) -> List[Dict[str, str]]:\n        \"\"\"\n        Returns a list of available pipes based on configuration.\n\n        Returns:\n            List of dictionaries containing pipe id and name.\n        \"\"\"\n        self.validate_environment()\n\n        # If a custom model is provided, use it exclusively.\n        if self.valves.AZURE_AI_MODEL:\n            self.name = \"Azure AI: \"\n            return [\n                {\"id\": self.valves.AZURE_AI_MODEL, \"name\": self.valves.AZURE_AI_MODEL}\n            ]\n\n        # If custom model is not provided but predefined models are enabled, return those.\n        if self.valves.USE_PREDEFINED_AZURE_AI_MODELS:\n            self.name = \"Azure AI: \"\n            return self.get_azure_models()\n\n        # Otherwise, use a default name.\n        return [{\"id\": \"Azure AI\", \"name\": \"Azure AI\"}]\n\n    async def pipe(\n        self, body: Dict[str, Any]\n    ) -> Union[str, Generator, Iterator, Dict[str, Any], StreamingResponse]:\n        \"\"\"\n        Main method for sending requests to the Azure AI endpoint.\n        The model name is passed as a header if defined.\n\n        Args:\n            body: The request body containing messages and other parameters\n\n        Returns:\n            Response from Azure AI API, which could be a string, dictionary or streaming response\n        \"\"\"\n        log = logging.getLogger(\"azure_ai.pipe\")\n        log.setLevel(SRC_LOG_LEVELS[\"OPENAI\"])\n\n        # Validate the request body\n        self.validate_body(body)\n\n        # Construct headers\n        headers = self.get_headers()\n\n        # Filter allowed parameters\n        allowed_params = {\n            \"model\",\n            \"messages\",\n            \"frequency_penalty\",\n            \"max_tokens\",\n            \"presence_penalty\",\n            \"response_format\",\n            \"seed\",\n            \"stop\",\n            \"stream\",\n            \"temperature\",\n            \"tool_choice\",\n            \"tools\",\n            \"top_p\",\n        }\n        filtered_body = {k: v for k, v in body.items() if k in allowed_params}\n\n        # If the valve indicates that the model name should be in the body,\n        # add it to the filtered body.\n        if self.valves.AZURE_AI_MODEL and self.valves.AZURE_AI_MODEL_IN_BODY:\n            filtered_body[\"model\"] = self.valves.AZURE_AI_MODEL\n        elif \"model\" in filtered_body and filtered_body[\"model\"]:\n            # Safer model extraction with split\n            filtered_body[\"model\"] = (\n                filtered_body[\"model\"].split(\".\", 1)[1]\n                if \".\" in filtered_body[\"model\"]\n                else filtered_body[\"model\"]\n            )\n\n        # Convert the modified body back to JSON\n        payload = json.dumps(filtered_body)\n\n        request = None\n        session = None\n        streaming = False\n        response = None\n\n        try:\n            session = aiohttp.ClientSession(\n                trust_env=True,\n                timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT),\n            )\n\n            request = await session.request(\n                method=\"POST\",\n                url=self.valves.AZURE_AI_ENDPOINT,\n                data=payload,\n                headers=headers,\n            )\n\n            # Check if response is SSE\n            if \"text/event-stream\" in request.headers.get(\"Content-Type\", \"\"):\n                streaming = True\n                return StreamingResponse(\n                    request.content,\n                    status_code=request.status,\n                    headers=dict(request.headers),\n                    background=BackgroundTask(\n                        cleanup_response, response=request, session=session\n                    ),\n                )\n            else:\n                try:\n                    response = await request.json()\n                except Exception as e:\n                    log.error(f\"Error parsing JSON response: {e}\")\n                    response = await request.text()\n\n                request.raise_for_status()\n                return response\n\n        except Exception as e:\n            log.exception(f\"Error in Azure AI request: {e}\")\n\n            detail = f\"Exception: {str(e)}\"\n            if isinstance(response, dict):\n                if \"error\" in response:\n                    detail = f\"{response['error']['message'] if 'message' in response['error'] else response['error']}\"\n            elif isinstance(response, str):\n                detail = response\n\n            return f\"Error: {detail}\"\n        finally:\n            if not streaming and session:\n                if request:\n                    request.close()\n                await session.close()\n",
    "meta": {
      "description": "Un pipeline pour interagir avec les services Azure AI, permettant une communication fluide avec divers modèles d'IA via des en-têtes configurables et une gestion robuste des erreurs. Cela inclut la prise en charge des modèles Azure OpenAI ainsi que d'autres modèles Azure AI grâce à la gestion dynamique des en-têtes et des configurations de requêtes. Prise en charge du chiffrement de la clé API.",
      "manifest": {
        "title": "Azure AI Foundry Pipeline",
        "author": "owndev",
        "author_url": "https://github.com/owndev",
        "project_url": "https://github.com/owndev/Open-WebUI-Functions",
        "funding_url": "https://github.com/owndev/Open-WebUI-Functions",
        "version": "2.1.0",
        "license": "Apache License 2.0",
        "description": "A pipeline for interacting with Azure AI services, enabling seamless communication with various AI models via configurable headers and robust error handling. This includes support for Azure OpenAI models as well as other Azure AI models by dynamically managing headers and request configurations.",
        "features": ""
      }
    },
    "is_active": true,
    "is_global": true,
    "updated_at": 1744725149,
    "created_at": 1744723109
  }
]

Additional Information

  • I have reviewed PR #11413 and PR #11412, which indicate that an internal validation or recalculation of the is_global flag may be applied. This is likely intended to ensure that only functions meeting certain security and validation criteria become global.
  • However, the documentation does not clearly specify what these criteria are.
  • Could you please provide clarification regarding the specific security or validation criteria that must be met for a function to remain global?
  • Additionally, are there any restrictions or internal logic that automatically reset the flag to false for certain function types (e.g., "pipe" functions) even when set to true in the manifest?

Thank you for your assistance.

Originally created by @EvKoh on GitHub (Apr 15, 2025). Original GitHub issue: https://github.com/open-webui/open-webui/issues/12899 ### Check Existing Issues - [x] I have searched the existing issues and discussions. - [x] I am using the latest version of Open WebUI. ### Installation Method Git Clone ### Open WebUI Version v0.6.5 ### Ollama Version (if applicable) _No response_ ### Operating System ubuntu, docker -> ghcr.io/open-webui/open-webui:latest ### Browser (if applicable) Version 135.0.7049.84 (Build officiel) snap (64 bits) ### Confirmation - [x] I have read and followed all instructions in `README.md`. - [x] I am using the latest version of **both** Open WebUI and Ollama. - [x] I have included the browser console logs. - [x] I have included the Docker container logs. - [x] I have listed steps to reproduce the bug in detail. ### Expected Behavior - When importing a function with `"is_global": true` defined in its manifest, the exported JSON and internal configuration should preserve this flag as `true`. - The function should remain globally available as specified in its manifest. ### Actual Behavior - After importing the function from [https://openwebui.com/f/owndev/azure_ai](https://openwebui.com/f/owndev/azure_ai) with `"is_global": true`, exporting the function later returns a JSON where `"is_global": false`. - This reset of the flag contradicts the initial configuration, preventing the function from remaining global. ### Steps to Reproduce 1. Import the function from [https://openwebui.com/f/owndev/azure_ai](https://openwebui.com/f/owndev/azure_ai) into your OpenWebUI instance, ensuring that the manifest contains `"is_global": true`. 2. Verify in the administration interface that the function appears as global (if applicable). 3. Export the function from the administration interface. 4. Open the exported JSON file and observe that the `is_global` field has been reset to `false`. ### Logs & Screenshots ![Image](https://github.com/user-attachments/assets/2e77c6f9-712d-4f92-9593-43ef97779aa0) This is a raw export of my function just before importing it. It clearly shows that is_global is correctly present with the value true in the initial import, but after exporting again from the interface, is_global is missing or automatically overwritten to false : ```json [ { "id": "azure_ai_global", "user_id": "c788549a-0cc0-4db3-9fe0-8d2af6b4c155", "name": "Azure AI Global", "type": "pipe", "content": "\"\"\"\ntitle: Azure AI Foundry Pipeline\nauthor: owndev\nauthor_url: https://github.com/owndev\nproject_url: https://github.com/owndev/Open-WebUI-Functions\nfunding_url: https://github.com/owndev/Open-WebUI-Functions\nversion: 2.1.0\nlicense: Apache License 2.0\ndescription: A pipeline for interacting with Azure AI services, enabling seamless communication with various AI models via configurable headers and robust error handling. This includes support for Azure OpenAI models as well as other Azure AI models by dynamically managing headers and request configurations.\nfeatures:\n - Supports dynamic model specification via headers.\n - Filters valid parameters to ensure clean requests.\n - Handles streaming and non-streaming responses.\n - Provides flexible timeout and error handling mechanisms.\n - Compatible with Azure OpenAI and other Azure AI models.\n - Predefined models for easy access.\n - Encrypted storage of sensitive API keys\n\"\"\"\n\nfrom typing import List, Union, Generator, Iterator, Optional, Dict, Any\nfrom fastapi.responses import StreamingResponse\nfrom pydantic import BaseModel, Field, GetCoreSchemaHandler\nfrom starlette.background import BackgroundTask\nfrom open_webui.env import AIOHTTP_CLIENT_TIMEOUT, SRC_LOG_LEVELS\nfrom cryptography.fernet import Fernet, InvalidToken\nimport aiohttp\nimport json\nimport os\nimport logging\nimport base64\nimport hashlib\nfrom pydantic_core import core_schema\n\n\n# Simplified encryption implementation with automatic handling\nclass EncryptedStr(str):\n \"\"\"A string type that automatically handles encryption/decryption\"\"\"\n\n @classmethod\n def _get_encryption_key(cls) -> Optional[bytes]:\n \"\"\"\n Generate encryption key from WEBUI_SECRET_KEY if available\n Returns None if no key is configured\n \"\"\"\n secret = os.getenv(\"WEBUI_SECRET_KEY\")\n if not secret:\n return None\n\n hashed_key = hashlib.sha256(secret.encode()).digest()\n return base64.urlsafe_b64encode(hashed_key)\n\n @classmethod\n def encrypt(cls, value: str) -> str:\n \"\"\"\n Encrypt a string value if a key is available\n Returns the original value if no key is available\n \"\"\"\n if not value or value.startswith(\"encrypted:\"):\n return value\n\n key = cls._get_encryption_key()\n if not key: # No encryption if no key\n return value\n\n f = Fernet(key)\n encrypted = f.encrypt(value.encode())\n return f\"encrypted:{encrypted.decode()}\"\n\n @classmethod\n def decrypt(cls, value: str) -> str:\n \"\"\"\n Decrypt an encrypted string value if a key is available\n Returns the original value if no key is available or decryption fails\n \"\"\"\n if not value or not value.startswith(\"encrypted:\"):\n return value\n\n key = cls._get_encryption_key()\n if not key: # No decryption if no key\n return value[len(\"encrypted:\") :] # Return without prefix\n\n try:\n encrypted_part = value[len(\"encrypted:\") :]\n f = Fernet(key)\n decrypted = f.decrypt(encrypted_part.encode())\n return decrypted.decode()\n except (InvalidToken, Exception):\n return value\n\n # Pydantic integration\n @classmethod\n def __get_pydantic_core_schema__(\n cls, _source_type: Any, _handler: GetCoreSchemaHandler\n ) -> core_schema.CoreSchema:\n return core_schema.union_schema(\n [\n core_schema.is_instance_schema(cls),\n core_schema.chain_schema(\n [\n core_schema.str_schema(),\n core_schema.no_info_plain_validator_function(\n lambda value: cls(cls.encrypt(value) if value else value)\n ),\n ]\n ),\n ],\n serialization=core_schema.plain_serializer_function_ser_schema(\n lambda instance: str(instance)\n ),\n )\n\n def get_decrypted(self) -> str:\n \"\"\"Get the decrypted value\"\"\"\n return self.decrypt(self)\n\n\n# Helper functions\nasync def cleanup_response(\n response: Optional[aiohttp.ClientResponse],\n session: Optional[aiohttp.ClientSession],\n) -> None:\n \"\"\"\n Clean up the response and session objects.\n\n Args:\n response: The ClientResponse object to close\n session: The ClientSession object to close\n \"\"\"\n if response:\n response.close()\n if session:\n await session.close()\n\n\nclass Pipe:\n # Environment variables for API key, endpoint, and optional model\n class Valves(BaseModel):\n # API key for Azure AI\n AZURE_AI_API_KEY: EncryptedStr = Field(\n default=os.getenv(\"AZURE_AI_API_KEY\", \"API_KEY\"),\n description=\"API key for Azure AI\",\n )\n\n # Endpoint for Azure AI (e.g. \"https://<your-endpoint>/chat/completions?api-version=2024-05-01-preview\" or \"https://<your-endpoint>/openai/deployments/gpt-4o/chat/completions?api-version=2024-08-01-preview\")\n AZURE_AI_ENDPOINT: str = Field(\n default=os.getenv(\n \"AZURE_AI_ENDPOINT\",\n \"https://<your-endpoint>/chat/completions?api-version=2024-05-01-preview\",\n ),\n description=\"Endpoint for Azure AI\",\n )\n\n # Optional model name, only necessary if not Azure OpenAI or if model name not in URL (e.g. \"https://<your-endpoint>/openai/deployments/<model-name>/chat/completions\")\n AZURE_AI_MODEL: str = Field(\n default=os.getenv(\"AZURE_AI_MODEL\", \"\"),\n description=\"Optional model name for Azure AI\",\n )\n\n # Switch for sending model name in request body\n AZURE_AI_MODEL_IN_BODY: bool = Field(\n default=os.getenv(\"AZURE_AI_MODEL_IN_BODY\", False),\n description=\"If True, include the model name in the request body instead of as a header.\",\n )\n\n # Flag to indicate if predefined Azure AI models should be used\n USE_PREDEFINED_AZURE_AI_MODELS: bool = Field(\n default=os.getenv(\"USE_PREDEFINED_AZURE_AI_MODELS\", False),\n description=\"Flag to indicate if predefined Azure AI models should be used.\",\n )\n\n # If True, use Authorization header with Bearer token instead of api-key header.\n USE_AUTHORIZATION_HEADER: bool = Field(\n default=bool(os.getenv(\"AZURE_AI_USE_AUTHORIZATION_HEADER\", False)),\n description=\"Set to True to use Authorization header with Bearer token instead of api-key header.\",\n )\n\n def __init__(self):\n self.valves = self.Valves()\n self.name: str = \"Azure AI\"\n\n def validate_environment(self) -> None:\n \"\"\"\n Validates that required environment variables are set.\n\n Raises:\n ValueError: If required environment variables are not set.\n \"\"\"\n # Access the decrypted API key\n api_key = self.valves.AZURE_AI_API_KEY.get_decrypted()\n if not api_key:\n raise ValueError(\"AZURE_AI_API_KEY is not set!\")\n if not self.valves.AZURE_AI_ENDPOINT:\n raise ValueError(\"AZURE_AI_ENDPOINT is not set!\")\n\n def get_headers(self) -> Dict[str, str]:\n \"\"\"\n Constructs the headers for the API request, including the model name if defined.\n\n Returns:\n Dictionary containing the required headers for the API request.\n \"\"\"\n # Access the decrypted API key\n api_key = self.valves.AZURE_AI_API_KEY.get_decrypted()\n if self.valves.USE_AUTHORIZATION_HEADER:\n headers = {\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\",\n }\n else:\n headers = {\"api-key\": api_key, \"Content-Type\": \"application/json\"}\n\n # If the valve indicates that the model name should be in the body,\n # add it to the filtered body.\n if self.valves.AZURE_AI_MODEL and not self.valves.AZURE_AI_MODEL_IN_BODY:\n headers[\"x-ms-model-mesh-model-name\"] = self.valves.AZURE_AI_MODEL\n return headers\n\n def validate_body(self, body: Dict[str, Any]) -> None:\n \"\"\"\n Validates the request body to ensure required fields are present.\n\n Args:\n body: The request body to validate\n\n Raises:\n ValueError: If required fields are missing or invalid.\n \"\"\"\n if \"messages\" not in body or not isinstance(body[\"messages\"], list):\n raise ValueError(\"The 'messages' field is required and must be a list.\")\n\n def get_azure_models(self) -> List[Dict[str, str]]:\n \"\"\"\n Returns a list of predefined Azure AI models.\n\n Returns:\n List of dictionaries containing model id and name.\n \"\"\"\n return [\n {\"id\": \"AI21-Jamba-1.5-Large\", \"name\": \"AI21 Jamba 1.5 Large\"},\n {\"id\": \"AI21-Jamba-1.5-Mini\", \"name\": \"AI21 Jamba 1.5 Mini\"},\n {\"id\": \"Codestral-2501\", \"name\": \"Codestral 25.01\"},\n {\"id\": \"Cohere-command-r\", \"name\": \"Cohere Command R\"},\n {\"id\": \"Cohere-command-r-08-2024\", \"name\": \"Cohere Command R 08-2024\"},\n {\"id\": \"Cohere-command-r-plus\", \"name\": \"Cohere Command R+\"},\n {\n \"id\": \"Cohere-command-r-plus-08-2024\",\n \"name\": \"Cohere Command R+ 08-2024\",\n },\n {\"id\": \"DeepSeek-R1\", \"name\": \"DeepSeek-R1\"},\n {\"id\": \"DeepSeek-V3\", \"name\": \"DeepSeek-V3\"},\n {\"id\": \"jais-30b-chat\", \"name\": \"JAIS 30b Chat\"},\n {\n \"id\": \"Llama-3.2-11B-Vision-Instruct\",\n \"name\": \"Llama-3.2-11B-Vision-Instruct\",\n },\n {\n \"id\": \"Llama-3.2-90B-Vision-Instruct\",\n \"name\": \"Llama-3.2-90B-Vision-Instruct\",\n },\n {\"id\": \"Llama-3.3-70B-Instruct\", \"name\": \"Llama-3.3-70B-Instruct\"},\n {\"id\": \"Meta-Llama-3-70B-Instruct\", \"name\": \"Meta-Llama-3-70B-Instruct\"},\n {\"id\": \"Meta-Llama-3-8B-Instruct\", \"name\": \"Meta-Llama-3-8B-Instruct\"},\n {\n \"id\": \"Meta-Llama-3.1-405B-Instruct\",\n \"name\": \"Meta-Llama-3.1-405B-Instruct\",\n },\n {\n \"id\": \"Meta-Llama-3.1-70B-Instruct\",\n \"name\": \"Meta-Llama-3.1-70B-Instruct\",\n },\n {\"id\": \"Meta-Llama-3.1-8B-Instruct\", \"name\": \"Meta-Llama-3.1-8B-Instruct\"},\n {\"id\": \"Ministral-3B\", \"name\": \"Ministral 3B\"},\n {\"id\": \"Mistral-large\", \"name\": \"Mistral Large\"},\n {\"id\": \"Mistral-large-2407\", \"name\": \"Mistral Large (2407)\"},\n {\"id\": \"Mistral-Large-2411\", \"name\": \"Mistral Large 24.11\"},\n {\"id\": \"Mistral-Nemo\", \"name\": \"Mistral Nemo\"},\n {\"id\": \"Mistral-small\", \"name\": \"Mistral Small\"},\n {\"id\": \"mistral-small-2503\", \"name\": \"Mistral Small 3.1\"},\n {\"id\": \"gpt-4o\", \"name\": \"OpenAI GPT-4o\"},\n {\"id\": \"gpt-4o-mini\", \"name\": \"OpenAI GPT-4o mini\"},\n {\"id\": \"o1\", \"name\": \"OpenAI o1\"},\n {\"id\": \"o1-mini\", \"name\": \"OpenAI o1-mini\"},\n {\"id\": \"o1-preview\", \"name\": \"OpenAI o1-preview\"},\n {\"id\": \"o3-mini\", \"name\": \"OpenAI o3-mini\"},\n {\n \"id\": \"Phi-3-medium-128k-instruct\",\n \"name\": \"Phi-3-medium instruct (128k)\",\n },\n {\"id\": \"Phi-3-medium-4k-instruct\", \"name\": \"Phi-3-medium instruct (4k)\"},\n {\"id\": \"Phi-3-mini-128k-instruct\", \"name\": \"Phi-3-mini instruct (128k)\"},\n {\"id\": \"Phi-3-mini-4k-instruct\", \"name\": \"Phi-3-mini instruct (4k)\"},\n {\"id\": \"Phi-3-small-128k-instruct\", \"name\": \"Phi-3-small instruct (128k)\"},\n {\"id\": \"Phi-3-small-8k-instruct\", \"name\": \"Phi-3-small instruct (8k)\"},\n {\"id\": \"Phi-3.5-mini-instruct\", \"name\": \"Phi-3.5-mini instruct (128k)\"},\n {\"id\": \"Phi-3.5-MoE-instruct\", \"name\": \"Phi-3.5-MoE instruct (128k)\"},\n {\"id\": \"Phi-3.5-vision-instruct\", \"name\": \"Phi-3.5-vision instruct (128k)\"},\n {\"id\": \"Phi-4\", \"name\": \"Phi-4\"},\n {\"id\": \"Phi-4-mini-instruct\", \"name\": \"Phi-4 mini instruct\"},\n {\"id\": \"Phi-4-multimodal-instruct\", \"name\": \"Phi-4 multimodal instruct\"},\n ]\n\n def pipes(self) -> List[Dict[str, str]]:\n \"\"\"\n Returns a list of available pipes based on configuration.\n\n Returns:\n List of dictionaries containing pipe id and name.\n \"\"\"\n self.validate_environment()\n\n # If a custom model is provided, use it exclusively.\n if self.valves.AZURE_AI_MODEL:\n self.name = \"Azure AI: \"\n return [\n {\"id\": self.valves.AZURE_AI_MODEL, \"name\": self.valves.AZURE_AI_MODEL}\n ]\n\n # If custom model is not provided but predefined models are enabled, return those.\n if self.valves.USE_PREDEFINED_AZURE_AI_MODELS:\n self.name = \"Azure AI: \"\n return self.get_azure_models()\n\n # Otherwise, use a default name.\n return [{\"id\": \"Azure AI\", \"name\": \"Azure AI\"}]\n\n async def pipe(\n self, body: Dict[str, Any]\n ) -> Union[str, Generator, Iterator, Dict[str, Any], StreamingResponse]:\n \"\"\"\n Main method for sending requests to the Azure AI endpoint.\n The model name is passed as a header if defined.\n\n Args:\n body: The request body containing messages and other parameters\n\n Returns:\n Response from Azure AI API, which could be a string, dictionary or streaming response\n \"\"\"\n log = logging.getLogger(\"azure_ai.pipe\")\n log.setLevel(SRC_LOG_LEVELS[\"OPENAI\"])\n\n # Validate the request body\n self.validate_body(body)\n\n # Construct headers\n headers = self.get_headers()\n\n # Filter allowed parameters\n allowed_params = {\n \"model\",\n \"messages\",\n \"frequency_penalty\",\n \"max_tokens\",\n \"presence_penalty\",\n \"response_format\",\n \"seed\",\n \"stop\",\n \"stream\",\n \"temperature\",\n \"tool_choice\",\n \"tools\",\n \"top_p\",\n }\n filtered_body = {k: v for k, v in body.items() if k in allowed_params}\n\n # If the valve indicates that the model name should be in the body,\n # add it to the filtered body.\n if self.valves.AZURE_AI_MODEL and self.valves.AZURE_AI_MODEL_IN_BODY:\n filtered_body[\"model\"] = self.valves.AZURE_AI_MODEL\n elif \"model\" in filtered_body and filtered_body[\"model\"]:\n # Safer model extraction with split\n filtered_body[\"model\"] = (\n filtered_body[\"model\"].split(\".\", 1)[1]\n if \".\" in filtered_body[\"model\"]\n else filtered_body[\"model\"]\n )\n\n # Convert the modified body back to JSON\n payload = json.dumps(filtered_body)\n\n request = None\n session = None\n streaming = False\n response = None\n\n try:\n session = aiohttp.ClientSession(\n trust_env=True,\n timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT),\n )\n\n request = await session.request(\n method=\"POST\",\n url=self.valves.AZURE_AI_ENDPOINT,\n data=payload,\n headers=headers,\n )\n\n # Check if response is SSE\n if \"text/event-stream\" in request.headers.get(\"Content-Type\", \"\"):\n streaming = True\n return StreamingResponse(\n request.content,\n status_code=request.status,\n headers=dict(request.headers),\n background=BackgroundTask(\n cleanup_response, response=request, session=session\n ),\n )\n else:\n try:\n response = await request.json()\n except Exception as e:\n log.error(f\"Error parsing JSON response: {e}\")\n response = await request.text()\n\n request.raise_for_status()\n return response\n\n except Exception as e:\n log.exception(f\"Error in Azure AI request: {e}\")\n\n detail = f\"Exception: {str(e)}\"\n if isinstance(response, dict):\n if \"error\" in response:\n detail = f\"{response['error']['message'] if 'message' in response['error'] else response['error']}\"\n elif isinstance(response, str):\n detail = response\n\n return f\"Error: {detail}\"\n finally:\n if not streaming and session:\n if request:\n request.close()\n await session.close()\n", "meta": { "description": "Un pipeline pour interagir avec les services Azure AI, permettant une communication fluide avec divers modèles d'IA via des en-têtes configurables et une gestion robuste des erreurs. Cela inclut la prise en charge des modèles Azure OpenAI ainsi que d'autres modèles Azure AI grâce à la gestion dynamique des en-têtes et des configurations de requêtes. Prise en charge du chiffrement de la clé API.", "manifest": { "title": "Azure AI Foundry Pipeline", "author": "owndev", "author_url": "https://github.com/owndev", "project_url": "https://github.com/owndev/Open-WebUI-Functions", "funding_url": "https://github.com/owndev/Open-WebUI-Functions", "version": "2.1.0", "license": "Apache License 2.0", "description": "A pipeline for interacting with Azure AI services, enabling seamless communication with various AI models via configurable headers and robust error handling. This includes support for Azure OpenAI models as well as other Azure AI models by dynamically managing headers and request configurations.", "features": "" } }, "is_active": true, "is_global": true, "updated_at": 1744725149, "created_at": 1744723109 } ] ``` ### Additional Information - I have reviewed PR [#11413](https://github.com/open-webui/open-webui/pull/11413) and PR [#11412](https://github.com/open-webui/open-webui/pull/11412), which indicate that an internal validation or recalculation of the `is_global` flag may be applied. This is likely intended to ensure that only functions meeting certain security and validation criteria become global. - However, the documentation does not clearly specify what these criteria are. - Could you please provide clarification regarding the specific security or validation criteria that must be met for a function to remain global? - Additionally, are there any restrictions or internal logic that automatically reset the flag to `false` for certain function types (e.g., "pipe" functions) even when set to true in the manifest? Thank you for your assistance.
GiteaMirror added the bug label 2026-04-19 22:36:18 -05:00
Author
Owner

@silentoplayz commented on GitHub (Apr 17, 2025):

I can confirm this issue. I tested with an Add to Memories action function with both the global toggle and the other toggle set to an On position. After exporting the function, deleting it from my Open WebUI instance, and finally imported it again, both toggles for the function were switched off.

<!-- gh-comment-id:2812669066 --> @silentoplayz commented on GitHub (Apr 17, 2025): I can confirm this issue. I tested with an `Add to Memories` action function with both the global toggle and the other toggle set to an `On` position. After exporting the function, deleting it from my Open WebUI instance, and finally imported it again, both toggles for the function were switched off.
Author
Owner

@tjbck commented on GitHub (Apr 28, 2025):

Intended behaviour, this should be considered as an extra security measure.

<!-- gh-comment-id:2835260034 --> @tjbck commented on GitHub (Apr 28, 2025): Intended behaviour, this should be considered as an extra security measure.
Author
Owner

@silentoplayz commented on GitHub (Apr 28, 2025):

Yeah, I could see how this behavior is rather intended after revisiting the issue thread.

<!-- gh-comment-id:2835810289 --> @silentoplayz commented on GitHub (Apr 28, 2025): Yeah, I could see how this behavior is rather intended after revisiting the issue thread.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/open-webui#16748