From 49e7eade15e4127f477c9c47d94696600f00e9e9 Mon Sep 17 00:00:00 2001 From: joaoback <156559121+joaoback@users.noreply.github.com> Date: Thu, 12 Feb 2026 22:42:52 -0300 Subject: [PATCH 01/34] Merge pull request #21345 from joaoback/patch-25 i18n: Update translation.json (pt-BR) --- src/lib/i18n/locales/pt-BR/translation.json | 382 ++++++++++---------- 1 file changed, 191 insertions(+), 191 deletions(-) diff --git a/src/lib/i18n/locales/pt-BR/translation.json b/src/lib/i18n/locales/pt-BR/translation.json index 3e000128f0..e83ab7b1f7 100644 --- a/src/lib/i18n/locales/pt-BR/translation.json +++ b/src/lib/i18n/locales/pt-BR/translation.json @@ -3,7 +3,7 @@ "'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "'s', 'm', 'h', 'd', 'w' ou '-1' para sem expiração.", "(e.g. `sh webui.sh --api --api-auth username_password`)": "(por exemplo, `sh webui.sh --api --api-auth username_password`)", "(e.g. `sh webui.sh --api`)": "(por exemplo, `sh webui.sh --api`)", - "(latest)": "(último)", + "(latest)": "(mais recente)", "(leave blank for to use commercial endpoint)": "(deixe em branco para usar o endpoint comercial)", "[Last] dddd [at] h:mm A": "[Último] dddd [em] h:mm A", "[Today at] h:mm A": "[Hoje às] h:mm A", @@ -20,14 +20,14 @@ "{{COUNT}} words": "{{COUNT}} palavras", "{{LOCALIZED_DATE}} at {{LOCALIZED_TIME}}": "{{LOCALIZED_DATE}} às {{LOCALIZED_TIME}}", "{{model}} download has been canceled": "O download do {{model}} foi cancelado", - "{{NAMES}} reacted with {{REACTION}}": "{{NAMES}} reagiu com {{REACTION}}", + "{{NAMES}} reacted with {{REACTION}}": "{{NAMES}} reagiram com {{REACTION}}", "{{user}}'s Chats": "Chats de {{user}}", "{{webUIName}} Backend Required": "Backend {{webUIName}} necessário", "*Prompt node ID(s) are required for image generation": "*Prompt node ID(s) são obrigatórios para gerar imagens", "1 Source": "1 Origem", "A collaboration channel where people join as members": "Um canal de colaboração onde as pessoas se juntam como membros.", "A discussion channel where access is controlled by groups and permissions": "Um canal de discussão onde o acesso é controlado por grupos e permissões.", - "A new version (v{{LATEST_VERSION}}) is now available.": "Um nova versão (v{{LATEST_VERSION}}) está disponível.", + "A new version (v{{LATEST_VERSION}}) is now available.": "Uma nova versão (v{{LATEST_VERSION}}) está disponível.", "A private conversation between you and selected users": "Uma conversa privada entre você e usuários selecionados.", "A task model is used when performing tasks such as generating titles for chats and web search queries": "Um modelo de tarefa é usado ao realizar tarefas como gerar títulos para chats e consultas de pesquisa na web", "a user": "um usuário", @@ -35,7 +35,7 @@ "Accept Autocomplete Generation\nJump to Prompt Variable": "Aceitar geração de autocompletar\nIr para a variável de prompt", "Access": "Acesso", "Access Control": "Controle de Acesso", - "Access List": "", + "Access List": "Lista de acesso", "Accessible to all users": "Acessível para todos os usuários", "Account": "Conta", "Account Activation Pending": "Ativação da Conta Pendente", @@ -53,8 +53,8 @@ "Add a model ID": "Adicione um ID de modelo", "Add a short description about what this model does": "Adicione uma descrição curta sobre o que este modelo faz", "Add a tag": "Adicionar uma tag", - "Add a tag...": "", - "Add Access": "", + "Add a tag...": "Adicione uma tag...", + "Add Access": "Adicionar acesso", "Add Arena Model": "Adicionar Modelo Arena", "Add Connection": "Adicionar Conexão", "Add Content": "Adicionar Conteúdo", @@ -63,13 +63,13 @@ "Add Custom Prompt": "Adicionar prompt personalizado", "Add Details": "Adicionar detalhes", "Add Files": "Adicionar Arquivos", - "Add Image": "", + "Add Image": "Adicionar imagem", "Add Member": "Adicionar membro", "Add Members": "Adicionar membros", "Add Memory": "Adicionar Memória", "Add Model": "Adicionar Modelo", "Add Reaction": "Adicionar reação", - "Add tag": "", + "Add tag": "Adicionar tag", "Add Tag": "Adicionar Tag", "Add text content": "Adicionar conteúdo de texto", "Add User": "Adicionar Usuário", @@ -78,7 +78,7 @@ "Additional Config": "Configuração adicional", "Additional configuration options for marker. This should be a JSON string with key-value pairs. For example, '{\"key\": \"value\"}'. Supported keys include: disable_links, keep_pageheader_in_output, keep_pagefooter_in_output, filter_blank_pages, drop_repeated_text, layout_coverage_threshold, merge_threshold, height_tolerance, gap_threshold, image_threshold, min_line_length, level_count, default_level": "Opções de configuração adicionais para o marcador. Deve ser uma string JSON com pares chave-valor. Por exemplo, '{\"key\": \"value\"}'. As chaves suportadas incluem: disable_links, keep_pageheader_in_output, keep_pagefooter_in_output, filter_blank_pages, drop_repeated_text, layout_coverage_threshold, merge_threshold, height_tolerance, gap_threshold, image_threshold, min_line_length, level_count, default_level", "Additional Parameters": "Parâmetros adicionais", - "Adds filenames, titles, sections, and snippets into the BM25 text to improve lexical recall.": "Adiciona nomes de arquivos, títulos, seções e trechos ao texto BM25 para melhorar a memorização lexical.", + "Adds filenames, titles, sections, and snippets into the BM25 text to improve lexical recall.": "Adiciona nomes de arquivos, títulos, seções e trechos ao texto BM25 para melhorar a recuperação lexical.", "Adjusting these settings will apply changes universally to all users.": "Ajustar essas configurações aplicará mudanças para todos os usuários.", "admin": "admin", "Admin": "Admin", @@ -94,7 +94,7 @@ "All": "Tudo", "All chats have been unarchived.": "Todos os chats foram desarquivados.", "All models deleted successfully": "Todos os modelos foram excluídos com sucesso", - "All Users": "", + "All Users": "Todos os usuários", "Allow Call": "Permitir chamada", "Allow Chat Controls": "Permitir Controles de Chats", "Allow Chat Delete": "Permitir Exclusão de Chats", @@ -129,7 +129,7 @@ "Amazing": "Incrível", "an assistant": "um assistente", "An error occurred while fetching the explanation": "Ocorreu um erro ao buscar a explicação", - "Analytics": "Análise", + "Analytics": "Análises", "Analyzed": "Analisado", "Analyzing...": "Analisando...", "and {{COUNT}} more": "e mais {{COUNT}}", @@ -145,29 +145,29 @@ "API Keys": "Chaves API", "API Mode": "Modo API", "API Timeout": "Tempo limite da API", - "API Type": "", + "API Type": "Tipo de API", "API Version": "Versão da API", "API Version is required": "Versão da API é obrigatória", "Application DN": "DN da Aplicação", "Application DN Password": "Senha da aplicação DN", - "applies to all users with the \"user\" role": "Aplicar para todos com permissão de \"usuário\"", + "applies to all users with the \"user\" role": "Aplica-se a todos com a função de \"usuário\"", "April": "Abril", "Archive": "Arquivar", - "Archive All": "", + "Archive All": "Arquivar tudo", "Archive All Chats": "Arquivar Todos os Chats", "Archived Chats": "Chats Arquivados", "archived-chat-export": "exportação de chats arquivados", - "Are you sure you want to archive all chats? This action cannot be undone.": "", + "Are you sure you want to archive all chats? This action cannot be undone.": "Tem certeza de que deseja arquivar todos os chats? Esta ação não pode ser desfeita.", "Are you sure you want to clear all memories? This action cannot be undone.": "Tem certeza de que deseja apagar todas as memórias? Esta ação não pode ser desfeita.", "Are you sure you want to delete \"{{NAME}}\"?": "Tem certeza de que deseja excluir \"{{NAME}}\"?", - "Are you sure you want to delete all chats? This action cannot be undone.": "", + "Are you sure you want to delete all chats? This action cannot be undone.": "Tem certeza de que deseja excluir todas as conversas? Esta ação não pode ser desfeita.", "Are you sure you want to delete this channel?": "Tem certeza de que deseja excluir este canal?", "Are you sure you want to delete this message?": "Tem certeza de que deseja excluir esta mensagem?", - "Are you sure you want to delete this version? Child versions will be relinked to this version's parent.": "", + "Are you sure you want to delete this version? Child versions will be relinked to this version's parent.": "Tem certeza de que deseja excluir esta versão? As versões filhas serão vinculadas novamente à versão pai.", "Are you sure you want to unarchive all archived chats?": "Você tem certeza que deseja desarquivar todos os chats arquivados?", "Arena Models": "Arena de Modelos", "Artifacts": "Artefatos", - "Asc": "", + "Asc": "Crescente", "Ask": "Perguntar", "Ask a question": "Faça uma pergunta", "Assistant": "Assistente", @@ -178,23 +178,23 @@ "Attach Webpage": "Anexar Página Web", "Attention to detail": "Atenção aos detalhes", "Attribute for Mail": "Atributo para E-mail", - "Attribute for Username": "Atribuir para nome de usuário", + "Attribute for Username": "Atributo para Nome de Usuário", "Audio": "Áudio", "August": "Agosto", - "Auth": "Aut.", + "Auth": "Autenticação", "Authenticate": "Autenticar", "Authentication": "Autenticação", "Auto": "Auto", "Auto (Random)": "Automático (Aleatório)", "Auto-Copy Response to Clipboard": "Cópia Automática da Resposta para a Área de Transferência", - "Auto-playback response": "Resposta de reprodução automática", + "Auto-playback response": "Reprodução automática da resposta", "Autocomplete Generation": "Geração de preenchimento automático", "Autocomplete Generation Input Max Length": "Comprimento máximo de entrada de geração de preenchimento automático", "Automatic1111": "Automatic1111", "AUTOMATIC1111 Api Auth String": "String de Autenticação da API AUTOMATIC1111", "AUTOMATIC1111 Base URL": "URL Base AUTOMATIC1111", "AUTOMATIC1111 Base URL is required.": "URL Base AUTOMATIC1111 é necessária.", - "Automatically inject system tools in native function calling mode (e.g., timestamps, memory, chat history, notes, etc.)": "Injetar automaticamente ferramentas do sistema no modo de chamada de função nativa (por exemplo, carimbos de data/hora, memória, histórico de bate-papo, notas, etc.)", + "Automatically inject system tools in native function calling mode (e.g., timestamps, memory, chat history, notes, etc.)": "Injetar automaticamente ferramentas do sistema no modo de chamada de função nativa (por exemplo, carimbos de data/hora, memória, histórico de chat, notas, etc.)", "Available list": "Lista disponível", "Available Tools": "Ferramentas disponíveis", "available users": "usuários disponíveis", @@ -224,7 +224,7 @@ "Boosting or penalizing specific tokens for constrained responses. Bias values will be clamped between -100 and 100 (inclusive). (Default: none)": "Aumentar ou penalizar tokens específicos para respostas restritas. Os valores de viés serão fixados entre -100 e 100 (inclusive). (Padrão: nenhum)", "Brave": "", "Brave Search API Key": "Chave API do Brave Search", - "Browse and query knowledge bases": "", + "Browse and query knowledge bases": "Navegue e consulte bases de conhecimento.", "Builtin Tools": "Ferramentas integradas", "Bullet List": "Lista com marcadores", "Button ID": "ID do botão", @@ -232,7 +232,7 @@ "Button Prompt": "Prompt do botão", "by {{name}}": "por {{name}}", "By {{name}}": "Por {{name}}", - "Bypass Embedding and Retrieval": "Ignorar incorporação e recuperação", + "Bypass Embedding and Retrieval": "Ignorar Embedding e Recuperação", "Bypass Web Loader": "Ignorar carregador da Web", "Cache Base Model List": "Lista de modelos base de cache", "Calendar": "Calendário", @@ -241,7 +241,7 @@ "Camera": "Câmera", "Cancel": "Cancelar", "Cannot create an empty note.": "Não é possível criar uma nota vazia.", - "Cannot delete the production version": "", + "Cannot delete the production version": "Não é possível excluir a versão de produção.", "Capabilities": "Capacidades", "Capture": "Capturar", "Capture Audio": "Capturar Audio", @@ -255,25 +255,25 @@ "Channel Type": "Tipo de canal", "Channel updated successfully": "Canal atualizado com sucesso", "Channels": "Canais", - "Character": "Caracter", + "Character": "Caractere", "Character limit for autocomplete generation input": "Limite de caracteres para entrada de geração de preenchimento automático", "Chart new frontiers": "Trace novas fronteiras", "Chat": "Chat", "Chat Background Image": "Imagem de Fundo do Chat", "Chat Bubble UI": "Interface de Bolha de Chat", - "Chat Completions": "", + "Chat Completions": "Gerar Resposta", "Chat Controls": "Controles de Chat", "Chat Conversation": "Conversa do Chat", "Chat direction": "Direção do Chat", - "Chat exported successfully": "", - "Chat History": "", + "Chat exported successfully": "Chat exportado com sucesso", + "Chat History": "Histórico de chat", "Chat ID": "ID do Chat", "Chat moved successfully": "Chat movido com sucesso", "Chat Overview": "Visão Geral do Chat", "Chat Permissions": "Permissões de Chat", "Chat Tags Auto-Generation": "Tags de Chat Geradas Automaticamente", - "Chat unshared successfully.": "", - "chats": "", + "Chat unshared successfully.": "Compartilhamento do chat removido com sucesso.", + "chats": "chats", "Chats": "Chats", "Check Again": "Verificar Novamente", "Check for updates": "Verificar atualizações", @@ -296,13 +296,13 @@ "Click here to download user import template file.": "Clique aqui para baixar o arquivo de modelo de importação de usuários.", "Click here to learn more about faster-whisper and see the available models.": "Clique aqui para aprender mais sobre Whisper e ver os modelos disponíveis.", "Click here to see available models.": "Clique aqui para ver os modelos disponíveis.", - "Click here to select": "Clique aqui para enviar", + "Click here to select": "Clique aqui para selecionar", "Click here to select a csv file.": "Clique aqui para enviar um arquivo csv.", "Click here to select a py file.": "Clique aqui para enviar um arquivo python.", "Click here to upload a workflow.json file.": "Clique aqui para enviar um arquivo workflow.json.", "click here.": "clique aqui.", - "Click on the user role button to change a user's role.": "Clique no botão de função do usuário para alterar a função de um usuário.", - "Click to copy ID": "", + "Click on the user role button to change a user's role.": "Clique no botão de perfil do usuário para alterar o perfil de um usuário.", + "Click to copy ID": "Clique para copiar o ID", "Clipboard write permission denied. Please check your browser settings to grant the necessary access.": "Permissão de escrita na área de transferência negada. Verifique as configurações do seu navegador para conceder o acesso necessário.", "Clone": "Clonar", "Clone Chat": "Clonar Chat", @@ -338,12 +338,12 @@ "ComfyUI Workflow": "", "ComfyUI Workflow Nodes": "", "Comma separated Node Ids (e.g. 1 or 1,2)": "IDs de Nodes separados por vírgula (por exemplo, 1 ou 1,2)", - "command": "", + "command": "comando", "Command": "Comando", "Comment": "Comentário", - "Commit Message": "", - "Community Reviews": "", - "Completions": "Conclusões", + "Commit Message": "Mensagem de Commit", + "Community Reviews": "Avaliações da comunidade", + "Completions": "Completions", "Compress Images in Channels": "Comprimir imagens em canais", "Concurrent Requests": "Solicitações simultâneas", "Config imported successfully": "Configuração importada com sucesso", @@ -370,7 +370,7 @@ "Continue with {{provider}}": "Continuar com {{provider}}", "Continue with Email": "Continuar com Email", "Continue with LDAP": "Continuar com LDAP", - "Control how message text is split for TTS requests. 'Punctuation' splits into sentences, 'paragraphs' splits into paragraphs, and 'none' keeps the message as a single string.": "Controlar como o texto do mensagem é dividido para solicitações TTS. 'Pontuação' dividida em frases, 'parágrafos' divide em parágrafos e 'não' mantém a mensagem como uma cadeia de caracteres.", + "Control how message text is split for TTS requests. 'Punctuation' splits into sentences, 'paragraphs' splits into paragraphs, and 'none' keeps the message as a single string.": "Controla como o texto da mensagem é dividido para solicitações TTS. 'Pontuação' divide em frases, 'Parágrafos' divide em parágrafos e 'Nenhum' mantém a mensagem como um texto único.", "Control the repetition of token sequences in the generated text. A higher value (e.g., 1.5) will penalize repetitions more strongly, while a lower value (e.g., 1.1) will be more lenient. At 1, it is disabled.": "Controla a repetição de sequências de tokens no texto gerado. Um valor mais alto (por exemplo, 1,5) penalizará as repetições com mais rigor, enquanto um valor mais baixo (por exemplo, 1,1) será mais tolerante. Em 1, ele está desabilitado.", "Controls": "Controles", "Controls the balance between coherence and diversity of the output. A lower value will result in more focused and coherent text.": "Controla o equilíbrio entre coerência e diversidade da saída. Um valor menor resultará em um texto mais focado e coerente.", @@ -385,8 +385,8 @@ "Copy Last Response": "Copiar última resposta", "Copy link": "Copiar link", "Copy Link": "Copiar Link", - "Copy Prompt": "", - "Copy Share Link": "", + "Copy Prompt": "Copiar prompt", + "Copy Share Link": "Copiar link de compartilhamento", "Copy to clipboard": "Copiar para a área de transferência", "Copy URL": "Copiar URL", "Copying to clipboard was successful!": "Cópia para a área de transferência bem-sucedida!", @@ -421,7 +421,7 @@ "Custom description enabled": "Descrição personalizada habilitada", "Custom Parameter Name": "Nome do parâmetro personalizado", "Custom Parameter Value": "Valor do parâmetro personalizado", - "Danger Zone": "Zona de perigo", + "Danger Zone": "Zona de Perigo", "Dark": "Escuro", "Data Controls": "Controle de Dados", "Database": "Banco de Dados", @@ -453,13 +453,13 @@ "Default User Role": "Padrão para novos usuários", "Delete": "Excluir", "Delete a model": "Excluir um modelo", - "Delete All": "", + "Delete All": "Excluir tudo", "Delete All Chats": "Excluir Todos os Chats", "Delete all contents inside this folder": "Apague todo o conteúdo desta pasta.", "Delete All Models": "Excluir Todos os Modelos", "Delete Chat": "Excluir Chat", "Delete chat?": "Excluir chat?", - "Delete File": "", + "Delete File": "Excluir arquivo", "Delete folder?": "Excluir pasta?", "Delete function?": "Excluir função?", "Delete Message": "Excluir mensagem", @@ -467,20 +467,20 @@ "Delete Model": "Excluir modelo", "Delete note?": "Excluir nota?", "Delete prompt?": "Excluir prompt?", - "Delete skill?": "", + "Delete skill?": "Excluir habilidade?", "delete this link": "Excluir este link", "Delete tool?": "Excluir ferramenta?", "Delete User": "Excluir Usuário", - "Delete Version": "", + "Delete Version": "Excluir versão", "Deleted": "Excluído", "Deleted {{deleteModelTag}}": "Excluído {{deleteModelTag}}", "Deleted {{name}}": "Excluído {{name}}", "Deleted User": "Usuário Excluído", "Deployment names are required for Azure OpenAI": "Nomes de implantação são necessários para o Azure OpenAI", - "Desc": "", - "Describe the edit...": "", - "Describe the image...": "", - "Describe what changed...": "", + "Desc": "Decrescente", + "Describe the edit...": "Descreva a edição...", + "Describe the image...": "Descreva a imagem...", + "Describe what changed...": "Descreva o que mudou...", "Describe your knowledge base and objectives": "Descreva sua base de conhecimento e objetivos", "Description": "Descrição", "Detect Artifacts Automatically": "Detectar artefatos automaticamente", @@ -488,11 +488,11 @@ "Didn't fully follow instructions": "Não seguiu completamente as instruções", "Direct": "Direto", "Direct Connections": "Conexões Diretas", - "Direct Connections allow users to connect to their own OpenAI compatible API endpoints.": "As conexões diretas permitem que os usuários se conectem aos seus próprios terminais de API compatíveis com OpenAI.", + "Direct Connections allow users to connect to their own OpenAI compatible API endpoints.": "As conexões diretas permitem que os usuários se conectem aos seus próprios endpoints de API compatíveis com OpenAI.", "Direct Message": "Mensagem direta", "Direct Tool Servers": "Servidores de ferramentas diretas", "Directory selection was cancelled": "A seleção do diretório foi cancelada", - "Disable All": "", + "Disable All": "Desativar tudo", "Disable Code Interpreter": "Desativar o interpretador de código", "Disable Image Extraction": "Desativar extração de imagem", "Disable image extraction from the PDF. If Use LLM is enabled, images will be automatically captioned. Defaults to False.": "Desabilite a extração de imagens do PDF. Se a opção Usar LLM estiver habilitada, as imagens serão legendadas automaticamente. O padrão é Falso.", @@ -529,12 +529,12 @@ "Documentation": "Documentação", "Documents": "Documentos", "does not make any external connections, and your data stays securely on your locally hosted server.": "não faz nenhuma conexão externa, e seus dados permanecem seguros no seu servidor local.", - "Domain Filter List": "Lista de filtros de domínio", + "Domain Filter List": "Lista de Filtros de Domínio", "don't fetch random pipelines from sources you don't trust.": "Não busque pipelines aleatórios de fontes não confiáveis.", "Don't have an account?": "Não tem uma conta?", - "don't install random functions from sources you don't trust.": "não instale funções aleatórias de origens que você não confia.", - "don't install random tools from sources you don't trust.": "não instale ferramentas aleatórias de origens que você não confia.", - "Don't like the style": "Não gosta do estilo", + "don't install random functions from sources you don't trust.": "não instale funções aleatórias de origens nas quais você não confia.", + "don't install random tools from sources you don't trust.": "não instale ferramentas aleatórias de origens nas quais você não confia.", + "Don't like the style": "Não gostei do estilo", "Done": "Concluído", "Download": "Baixar", "Download & Delete": "Baixar e excluir", @@ -552,20 +552,20 @@ "e.g. A filter to remove profanity from text": "Exemplo: Um filtro para remover palavrões do texto", "e.g. about the Roman Empire": "Por exemplo, sobre o Império Romano.", "e.g. alloy, echo, shimmer": "por exemplo alloy, echo, shimmer", - "e.g. Code Review Guidelines": "", - "e.g. code-review-guidelines": "", + "e.g. Code Review Guidelines": "Exemplo: Diretrizes de Revisão de Código", + "e.g. code-review-guidelines": "por exemplo, diretrizes de revisão de código", "e.g. en": "por exemplo, en", "e.g. My Filter": "Exemplo: Meu Filtro", "e.g. My Tools": "Exemplo: Minhas Ferramentas", "e.g. my_filter": "Exemplo: my_filter", "e.g. my_tools": "Exemplo: my_tools", "e.g. pdf, docx, txt": "por exemplo, pdf, docx, txt", - "e.g. Step-by-step instructions for code reviews": "", + "e.g. Step-by-step instructions for code reviews": "Por exemplo, instruções passo a passo para revisões de código.", "e.g. Tell me a fun fact": "Por exemplo: Conte-me uma curiosidade.", "e.g. Tell me a fun fact about the Roman Empire": "Por exemplo: Conte-me uma curiosidade sobre o Império Romano.", "e.g. Tools for performing various operations": "Exemplo: Ferramentas para executar operações diversas", "e.g., 3, 4, 5 (leave blank for default)": "por exemplo, 3, 4, 5 (deixe em branco para o padrão)", - "e.g., audio/wav,audio/mpeg,video/* (leave blank for defaults)": "por exemplo, áudio/wav, áudio/mpeg, vídeo/* (deixe em branco para os padrões)", + "e.g., audio/wav,audio/mpeg,video/* (leave blank for defaults)": "por exemplo, audio/wav, audio/mpeg, video/* (deixe em branco para os padrões)", "e.g., en-US,ja-JP (leave blank for auto-detect)": "por exemplo, en-US, ja-JP (deixe em branco para detecção automática)", "e.g., westus (leave blank for eastus)": "por exemplo, westus (deixe em branco para eastus)", "Edit": "Editar", @@ -577,7 +577,7 @@ "Edit Image": "Editar imagem", "Edit Last Message": "Editar última mensagem", "Edit Memory": "Editar Memória", - "Edit Prompt": "", + "Edit Prompt": "Editar prompt", "Edit User": "Editar Usuário", "Edit User Group": "Editar Grupo de Usuários", "Edit workflow.json content": "Editar conteúdo do workflow.json", @@ -592,19 +592,19 @@ "Embedding Batch Size": "Tamanho do Lote de Embedding", "Embedding Model": "Modelo de Embedding", "Embedding Model Engine": "Motor do Modelo de Embedding", - "Enable All": "", - "Enable API Keys": "Habilitar chave de API", + "Enable All": "Ativar tudo", + "Enable API Keys": "Habilitar Chaves de API", "Enable autocomplete generation for chat messages": "Habilitar geração de preenchimento automático para mensagens do chat", "Enable Code Execution": "Habilitar execução de código", "Enable Code Interpreter": "Habilitar intérprete de código", "Enable Community Sharing": "Ativar Compartilhamento com a Comunidade", "Enable Memory Locking (mlock) to prevent model data from being swapped out of RAM. This option locks the model's working set of pages into RAM, ensuring that they will not be swapped out to disk. This can help maintain performance by avoiding page faults and ensuring fast data access.": "Habilite o bloqueio de memória (mlock) para evitar que os dados do modelo sejam transferidos da RAM para a área de troca (swap). Essa opção bloqueia o conjunto de páginas em uso pelo modelo na RAM, garantindo que elas não sejam transferidas para o disco. Isso pode ajudar a manter o desempenho, evitando falhas de página e garantindo acesso rápido aos dados.", "Enable Memory Mapping (mmap) to load model data. This option allows the system to use disk storage as an extension of RAM by treating disk files as if they were in RAM. This can improve model performance by allowing for faster data access. However, it may not work correctly with all systems and can consume a significant amount of disk space.": "Habilite o mapeamento de memória (mmap) para carregar dados do modelo. Esta opção permite que o sistema use o armazenamento em disco como uma extensão da RAM, tratando os arquivos do disco como se estivessem na RAM. Isso pode melhorar o desempenho do modelo, permitindo acesso mais rápido aos dados. No entanto, pode não funcionar corretamente com todos os sistemas e consumir uma quantidade significativa de espaço em disco.", - "Enable Message Queue": "", + "Enable Message Queue": "Habilitar fila de mensagens", "Enable Message Rating": "Ativar Avaliação de Mensagens", - "Enable Mirostat sampling for controlling perplexity.": "", + "Enable Mirostat sampling for controlling perplexity.": "Habilitar amostragem Mirostat para controlar a perplexidade.", "Enable New Sign Ups": "Ativar Novos Cadastros", - "Enable, disable, or customize the reasoning tags used by the model. \"Enabled\" uses default tags, \"Disabled\" turns off reasoning tags, and \"Custom\" lets you specify your own start and end tags.": "Habilite, desabilite ou personalize as tags de raciocínio usadas pelo modelo. \"Enabled\" usa tags padrão, \"Disabled\" desativa as tags de raciocínio e \"Custom\" permite que você especifique suas próprias tags de início e fim.", + "Enable, disable, or customize the reasoning tags used by the model. \"Enabled\" uses default tags, \"Disabled\" turns off reasoning tags, and \"Custom\" lets you specify your own start and end tags.": "Habilite, desabilite ou personalize as tags de raciocínio usadas pelo modelo. \"Ativado\" usa tags padrão, \"Desativado\" desativa as tags de raciocínio e \"Personalizado\" permite que você especifique suas próprias tags de início e fim.", "Enabled": "Ativado", "End Tag": "Tag final", "Endpoint URL": "", @@ -651,9 +651,9 @@ "Enter External Web Search URL": "Insira a URL de pesquisa na Web externa", "Enter Firecrawl API Base URL": "Insira a URL base da API do Firecrawl", "Enter Firecrawl API Key": "Insira a chave da API do Firecrawl", - "Enter Firecrawl Timeout": "Tempo limite do Firecrawl", + "Enter Firecrawl Timeout": "Insira o Tempo Limite do Firecrawl", "Enter folder name": "Digite o nome da pasta", - "Enter function name filter list (e.g. func1, !func2)": "", + "Enter function name filter list (e.g. func1, !func2)": "Insira a lista de filtros de nomes de funções (por exemplo, func1, !func2)", "Enter Github Raw URL": "Digite a URL bruta do Github", "Enter Google PSE API Key": "Digite a Chave API do Google PSE", "Enter Google PSE Engine Id": "Digite o ID do Motor do Google PSE", @@ -684,7 +684,7 @@ "Enter Playwright Timeout": "", "Enter Playwright WebSocket URL": "", "Enter proxy URL (e.g. https://user:password@host:port)": "Insira a URL do proxy (por exemplo, https://usuário:senha@host:porta)", - "Enter reasoning effort": "Enter reasoning effort", + "Enter reasoning effort": "Insira o esforço de raciocínio", "Enter Score": "Digite a Pontuação", "Enter SearchApi API Key": "Digite a Chave API do SearchApi", "Enter SearchApi Engine": "Digite o Motor do SearchApi", @@ -699,7 +699,7 @@ "Enter server host": "Digite o host do servidor", "Enter server label": "Digite o label do servidor", "Enter server port": "Digite a porta do servidor", - "Enter skill instructions in markdown...": "", + "Enter skill instructions in markdown...": "Insira as instruções da habilidade em Markdown...", "Enter Sougou Search API sID": "Insira o sID da API de pesquisa do Sougou", "Enter Sougou Search API SK": "Digite Sougou Search API SK", "Enter stop sequence": "Digite a sequência de parada", @@ -722,8 +722,8 @@ "Enter Yacy Password": "Digite a senha do Yacy", "Enter Yacy URL (e.g. http://yacy.example.com:8090)": "Digite a URL do Yacy (por exemplo, http://yacy.example.com:8090)", "Enter Yacy Username": "Digite o nome de usuário do Yacy", - "Enter Yandex Web Search API Key": "", - "Enter Yandex Web Search URL": "", + "Enter Yandex Web Search API Key": "Insira a chave da API de pesquisa da Yandex.", + "Enter Yandex Web Search URL": "Insira o URL de pesquisa da Yandex Web", "Enter your code here...": "Insira seu código aqui...", "Enter your current password": "Digite sua senha atual", "Enter Your Email": "Digite Seu Email", @@ -752,12 +752,12 @@ "Exa API Key": "", "Example: (&(objectClass=inetOrgPerson)(uid=%s))": "Exemplo: (&(objectClass=inetOrgPerson)(uid=%s))", "Example: ALL": "Exemplo: ALL", - "Example: mail": "Exemplo: Email", + "Example: mail": "Exemplo: mail", "Example: ou=users,dc=foo,dc=example": "Exemplo: ou=users,dc=foo,dc=example", "Example: sAMAccountName or uid or userPrincipalName": "Exemplo: sAMAccountName ou uid ou userPrincipalName", "Exceeded the number of seats in your license. Please contact support to increase the number of seats.": "Excedeu o número de licenças disponíveis. Entre em contato com o suporte para aumentar o número de licenças.", "Exclude": "Excluir", - "Execute code": "", + "Execute code": "Executar código", "Execute code for analysis": "Executar código para análise", "Executing **{{NAME}}**...": "Executando **{{NAME}}**...", "Expand": "Expandir", @@ -784,7 +784,7 @@ "External Web Loader URL": "URL do carregador da Web externo", "External Web Search API Key": "Chave de API de pesquisa na Web externa", "External Web Search URL": "URL de pesquisa na Web externa", - "Fade Effect for Streaming Text": "Efeito de desbotamento para texto em streaming", + "Fade Effect for Streaming Text": "Efeito de Fade para texto em streaming", "Failed to add file.": "Falha ao adicionar arquivo.", "Failed to add members": "Falha ao adicionar membros", "Failed to clear status": "Falha ao limpar o status", @@ -792,7 +792,7 @@ "Failed to copy link": "Falha ao copiar o link", "Failed to create API Key.": "Falha ao criar a Chave API.", "Failed to delete note": "Falha ao excluir a nota", - "Failed to download image": "", + "Failed to download image": "Falha ao baixar a imagem", "Failed to extract content from the file: {{error}}": "Falha ao extrair conteúdo do arquivo: {{error}}", "Failed to extract content from the file.": "Falha ao extrair conteúdo do arquivo.", "Failed to fetch models": "Falha ao buscar modelos", @@ -810,7 +810,7 @@ "Failed to save connections": "Falha ao salvar conexões", "Failed to save conversation": "Falha ao salvar a conversa", "Failed to save models configuration": "Falha ao salvar a configuração dos modelos", - "Failed to unshare chat.": "", + "Failed to unshare chat.": "Falha ao cancelar o compartilhamento da conversa.", "Failed to update settings": "Falha ao atualizar as configurações", "Failed to update status": "Falha ao atualizar o status", "Failed to upload file.": "Falha ao carregar o arquivo.", @@ -818,7 +818,7 @@ "Features Permissions": "Permissões das Funcionalidades", "February": "Fevereiro", "Feedback": "", - "Feedback Activity": "", + "Feedback Activity": "Atividade de feedback", "Feedback deleted successfully": "O feedback foi excluído com sucesso.", "Feedback Details": "Detalhes do comentário", "Feedback History": "Histórico de comentários", @@ -826,9 +826,9 @@ "Female": "Feminino", "File": "Arquivo", "File added successfully.": "Arquivo adicionado com sucesso.", - "File content updated successfully.": "Arquivo de conteúdo atualizado com sucesso.", + "File content updated successfully.": "Conteúdo do arquivo atualizado com sucesso.", "File Context": "Contexto do arquivo", - "File deleted successfully.": "", + "File deleted successfully.": "Arquivo excluído com sucesso.", "File Mode": "Modo de Arquivo", "File not found.": "Arquivo não encontrado.", "File removed successfully.": "Arquivo removido com sucesso.", @@ -836,7 +836,7 @@ "File Upload": "Upload de arquivo", "File uploaded successfully": "Arquivo carregado com sucesso", "File uploaded!": "Arquivo enviado!", - "Filename": "", + "Filename": "Nome do arquivo", "Files": "Arquivos", "Filter": "Filtro", "Filter is now globally disabled": "O filtro está agora desativado globalmente", @@ -862,7 +862,7 @@ "Follow Up Generation Prompt": "Prompt para Geração dos Acompanhamentos", "Follow-Up Auto-Generation": "Geração automática de acompanhamento", "Followed instructions perfectly": "Seguiu as instruções perfeitamente", - "for placeholders": "", + "for placeholders": "para espaços reservados", "Force OCR": "Forçar OCR", "Force OCR on all pages of the PDF. This can lead to worse results if you have good text in your PDFs. Defaults to False.": "Forçar OCR em todas as páginas do PDF. Isso pode levar a resultados piores se você tiver texto de boa qualidade nos seus PDFs. O padrão é Falso.", "Forge new paths": "Trilhar novos caminhos", @@ -897,14 +897,14 @@ "General": "Geral", "Generate": "Gerar", "Generate an image": "Gerar uma imagem", - "Generate and edit images": "", + "Generate and edit images": "Gere e edite imagens", "Generate Message Pair": "Gerar par de mensagens", "Generated Image": "Imagem gerada", - "Generated images will appear here": "", + "Generated images will appear here": "As imagens geradas aparecerão aqui.", "Generating search query": "Gerando consulta de pesquisa", "Generating...": "Gerando...", - "Get current time and perform date/time calculations": "", - "Get information on {{name}} in the UI": "Obtenha informações sobre {{name}} na IU", + "Get current time and perform date/time calculations": "Obtenha a hora atual e realize cálculos de data/hora.", + "Get information on {{name}} in the UI": "Obtenha informações sobre {{name}} na interface", "Get started": "Iniciar", "Get started with {{WEBUI_NAME}}": "Iniciar com {{WEBUI_NAME}}", "Global": "Global", @@ -927,23 +927,23 @@ "Groups": "Grupos", "H1": "Título", "H2": "Subtítulo", - "H3": "Sub-subtítulos", + "H3": "Sub-subtítulo", "Haptic Feedback": "Feedback Tátil", "Headers": "Cabeçalhos", "Headers must be a valid JSON object": "Os cabeçalhos devem ser um objeto JSON válido", "Height": "Altura", "Hello, {{name}}": "Olá, {{name}}", "Help": "Ajuda", - "Help the community discover great models": "", + "Help the community discover great models": "Ajude a comunidade a descobrir ótimos modelos.", "Hex Color": "Cor hexadecimal", "Hex Color - Leave empty for default color": "Cor Hexadecimal - Deixe em branco para a cor padrão", - "Hidden": "", + "Hidden": "Escondido", "Hide": "Ocultar", "Hide from Sidebar": "Ocultar da barra lateral", "Hide Model": "Ocultar modelo", "High": "Alto", "High Contrast Mode": "Modo de alto contraste", - "History": "", + "History": "Histórico", "Home": "Início", "Host": "Servidor", "How can I help you today?": "Como posso ajudar você hoje?", @@ -955,7 +955,7 @@ "I acknowledge that I have read and I understand the implications of my action. I am aware of the risks associated with executing arbitrary code and I have verified the trustworthiness of the source.": "Eu reconheço que li e entendi as implicações da minha ação. Estou ciente dos riscos associados à execução de código arbitrário e verifiquei a confiabilidade da fonte.", "ID": "", "ID cannot contain \":\" or \"|\" characters": "O ID não pode conter caracteres \":\" ou \"|\"", - "ID copied to clipboard": "", + "ID copied to clipboard": "ID copiado para a área de transferência", "iframe Sandbox Allow Forms": "", "iframe Sandbox Allow Same Origin": "", "Ignite curiosity": "Desperte a curiosidade", @@ -965,7 +965,7 @@ "Image Compression Width": "Largura de compressão de imagem", "Image Edit": "Edição de imagem", "Image Edit Engine": "Motor de edição de imagens", - "Image Generation": "Geração de Imagem", + "Image Generation": "Geração de Imagens", "Image Generation Engine": "Motor de Geração de Imagem", "Image Max Compression Size": "Tamanho máximo de compressão da imagem", "Image Max Compression Size height": "Altura do tamanho máximo de compressão da imagem", @@ -984,7 +984,7 @@ "Import successful": "Importação bem-sucedida", "Import Tools": "Importar Ferramentas", "Important Update": "Atualização importante", - "Inactive": "", + "Inactive": "Inativo", "Include": "Incluir", "Include `--api-auth` flag when running stable-diffusion-webui": "Incluir a flag `--api-auth` ao executar stable-diffusion-webui", "Include `--api` flag when running stable-diffusion-webui": "Incluir a flag `--api` ao executar stable-diffusion-webui", @@ -1022,7 +1022,7 @@ "January": "Janeiro", "Jina API Base URL": "", "Jina API Key": "Chave de API Jina", - "join our Discord for help.": "junte-se ao nosso Discord para ajudar.", + "join our Discord for help.": "junte-se ao nosso Discord para obter ajuda.", "JSON": "JSON", "JSON Preview": "Pré-visualização JSON", "JSON Spec": "Especificação JSON", @@ -1066,15 +1066,15 @@ "Learn More": "Saiba Mais", "Learn more about OpenAPI tool servers.": "Saiba mais sobre servidores de ferramentas OpenAPI.", "Learn more about Voxtral transcription.": "Saiba mais sobre a transcrição do Voxtral.", - "Leave a public review for {{modelName}}": "", + "Leave a public review for {{modelName}}": "Deixe uma avaliação pública para {{modelName}}", "Leave empty for no compression": "Deixe em branco para nenhuma compactação", "Leave empty for unlimited": "Deixe vazio para ilimitado", - "Leave empty to include all models from \"{{url}}\" endpoint": "Deixe em branco para incluir todos os modelos do ponto final \"{{url}}\"", - "Leave empty to include all models from \"{{url}}/api/tags\" endpoint": "Deixe em branco para incluir todos os modelos do ponto final \"{{url}}/api/tags\"", - "Leave empty to include all models from \"{{url}}/models\" endpoint": "Deixe em branco para incluir todos os modelos do ponto final \"{{url}}/models\"", - "Leave empty to include all models or select specific models": "Deixe vazio para incluir todos os modelos ou selecione modelos especificos", + "Leave empty to include all models from \"{{url}}\" endpoint": "Deixe em branco para incluir todos os modelos do endpoint \"{{url}}\"", + "Leave empty to include all models from \"{{url}}/api/tags\" endpoint": "Deixe em branco para incluir todos os modelos do endpoint \"{{url}}/api/tags\"", + "Leave empty to include all models from \"{{url}}/models\" endpoint": "Deixe em branco para incluir todos os modelos do endpoint \"{{url}}/models\"", + "Leave empty to include all models or select specific models": "Deixe vazio para incluir todos os modelos ou selecione modelos específicos", "Leave empty to use first admin user": "Deixe em branco para usar o primeiro usuário administrador.", - "Leave empty to use the default config, or enter a valid json (see https://yandex.cloud/en/docs/search-api/api-ref/WebSearch/search#yandex.cloud.searchapi.v2.WebSearchRequest)": "", + "Leave empty to use the default config, or enter a valid json (see https://yandex.cloud/en/docs/search-api/api-ref/WebSearch/search#yandex.cloud.searchapi.v2.WebSearchRequest)": "Deixe em branco para usar a configuração padrão ou insira um JSON válido (consulte https://yandex.cloud/en/docs/search-api/api-ref/WebSearch/search#yandex.cloud.searchapi.v2.WebSearchRequest)", "Leave empty to use the default model (voxtral-mini-latest).": "Deixe em branco para usar o modelo padrão (voxtral-mini-latest).", "Leave empty to use the default prompt, or enter a custom prompt": "Deixe vazio para usar o prompt padrão, ou insira um prompt personalizado", "Leave model field empty to use the default model.": "Deixe o campo do modelo vazio para usar o modelo padrão.", @@ -1086,7 +1086,7 @@ "Limit concurrent search queries. 0 = unlimited (default). Set to 1 for sequential execution (recommended for APIs with strict rate limits like Brave free tier).": "Limitar consultas de pesquisa simultâneas. 0 = ilimitado (padrão). Defina como 1 para execução sequencial (recomendado para APIs com limites de taxa rígidos, como o nível gratuito do Brave).", "List": "Lista", "Listening...": "Escutando...", - "Live": "", + "Live": "Ao vivo", "Llama.cpp": "", "LLMs can make mistakes. Verify important information.": "LLMs podem cometer erros. Verifique informações importantes.", "Loader": "Carregador", @@ -1105,7 +1105,7 @@ "Male": "Masculino", "Manage": "Gerenciar", "Manage Direct Connections": "Gerenciar conexões diretas", - "Manage Files": "", + "Manage Files": "Gerenciar arquivos", "Manage Models": "Gerenciar modelos", "Manage Ollama": "Gerenciar Ollama", "Manage Ollama API Connections": "Gerenciar Conexões Ollama API", @@ -1128,7 +1128,7 @@ "MCP support is experimental and its specification changes often, which can lead to incompatibilities. OpenAPI specification support is directly maintained by the Open WebUI team, making it the more reliable option for compatibility.": "O suporte ao MCP é experimental e suas especificações mudam com frequência, o que pode levar a incompatibilidades. O suporte à especificação OpenAPI é mantido diretamente pela equipe do Open WebUI, tornando-o a opção mais confiável para compatibilidade.", "Medium": "Médio", "Member removed successfully": "Membro removido com sucesso", - "members": "", + "members": "membros", "Members": "Membros", "Members added successfully": "Membros adicionados com sucesso", "Memories": "Memórias", @@ -1142,10 +1142,10 @@ "Merged Response": "Resposta Mesclada", "Message": "Mensagem", "Message counts and response timestamps": "Contagem de mensagens e registros de data e hora de resposta", - "Message counts are based on assistant responses.": "", - "Message rating should be enabled to use this feature": "Mensagem de avaliação deve estar habilitada para usar esta função", - "messages": "", - "Messages": "", + "Message counts are based on assistant responses.": "A contagem de mensagens é baseada nas respostas do assistente.", + "Message rating should be enabled to use this feature": "A avaliação de mensagens deve estar habilitada para usar este recurso", + "messages": "mensagens", + "Messages": "Mensagens", "Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "Mensagens enviadas após criar seu link não serão compartilhadas. Usuários com o URL poderão visualizar o chat compartilhado.", "Microsoft OneDrive": "", "Microsoft OneDrive (personal)": "", @@ -1170,7 +1170,7 @@ "Model can search the web for information": "O modelo pode pesquisar informações na web", "Model created successfully!": "Modelo criado com sucesso!", "Model filesystem path detected. Model shortname is required for update, cannot continue.": "Caminho do sistema de arquivos do modelo detectado. Nome curto do modelo é necessário para atualização, não é possível continuar.", - "Model Filtering": "Filtrando modelo", + "Model Filtering": "Filtragem de Modelos", "Model ID": "ID do Modelo", "Model ID is required.": "É necessário o ID do modelo.", "Model IDs": "IDs do modelo", @@ -1178,14 +1178,14 @@ "Model name already exists, please choose a different one": "O nome do modelo já existe, escolha um diferente", "Model Name is required.": "O nome do modelo é obrigatório.", "Model names and usage frequency": "Nomes dos modelos e frequência de utilização", - "Model not found": "", + "Model not found": "Modelo não encontrado", "Model not selected": "Modelo não selecionado", "Model Params": "Parâmetros do Modelo", "Model Permissions": "Permissões do Modelo", "Model responses or outputs": "Respostas ou resultados do modelo", "Model unloaded successfully": "Modelo descarregado com sucesso", "Model updated successfully": "Modelo atualizado com sucesso", - "Model Usage": "", + "Model Usage": "Utilização do modelo", "Model(s) do not support file upload": "Modelo(s) não suportam upload de arquivo", "Modelfile Content": "Conteúdo do Arquivo do Modelo", "Models": "Modelos", @@ -1195,14 +1195,14 @@ "Models Public Sharing": "Compartilhamento Público de Modelos", "Models Sharing": "Compartilhamento de Modelos", "Mojeek": "", - "Mojeek Search API Key": "Chave de API Mojeel Search", + "Mojeek Search API Key": "Chave de API Mojeek Search", "More": "Mais", "More Concise": "Mais conciso", - "More options": "", + "More options": "Mais opções", "More Options": "Mais opções", "Move": "Mover", "Name": "Nome", - "Name and ID are required, please fill them out": "Nome e documento de identidade são obrigatórios, por favor preencha-os", + "Name and ID are required, please fill them out": "Nome e ID são obrigatórios, por favor preencha-os", "Name your knowledge base": "Nome da sua base de conhecimento", "Native": "Nativo", "New": "Novo", @@ -1215,13 +1215,13 @@ "New Note": "Nova nota", "New Password": "Nova Senha", "New Prompt": "Novo Prompt", - "New Skill": "", + "New Skill": "Nova habilidade", "New Temporary Chat": "Novo chat temporário", - "New Tool": "Nova Ferrameta", + "New Tool": "Nova Ferramenta", "New Webhook": "Novo Webhook", "new-channel": "novo-canal", "Next message": "Próxima mensagem", - "No access grants. Private to you.": "", + "No access grants. Private to you.": "Sem permissões de acesso. Privacidade exclusiva para você.", "No activity data": "Sem dados de atividade", "No authentication": "Sem autenticação", "No chats found": "Nenhum chat encontrado", @@ -1231,16 +1231,16 @@ "No content found": "Nenhum conteúdo encontrado", "No content to speak": "Sem conteúdo para falar", "No conversation to save": "Nenhuma conversa para salvar", - "No data": "", - "No data found": "", + "No data": "Sem dados", + "No data found": "Nenhum dado encontrado", "No distance available": "Sem distância disponível", - "No expiration can pose security risks.": "Nenhuma expiração pode representar riscos de segurança.", + "No expiration can pose security risks.": "A ausência de expiração pode representar riscos de segurança.", "No feedback found": "Nenhum feedback encontrado", "No file selected": "Nenhum arquivo selecionado", - "No files found": "", + "No files found": "Nenhum arquivo encontrado", "No files in this knowledge base.": "Não existem arquivos nesta base de conhecimento.", "No functions found": "Nenhuma função encontrada", - "No history available": "", + "No history available": "Não há histórico disponível.", "No HTML, CSS, or JavaScript content found.": "Nenhum conteúdo HTML, CSS ou JavaScript encontrado.", "No inference engine with management support found": "Nenhum mecanismo de inferência com suporte de gerenciamento encontrado", "No knowledge bases found.": "Nenhuma base de conhecimento encontrada.", @@ -1257,7 +1257,7 @@ "No results": "Nenhum resultado encontrado", "No results found": "Nenhum resultado encontrado", "No search query generated": "Nenhuma consulta de pesquisa gerada", - "No skills found": "", + "No skills found": "Nenhuma habilidade encontrada", "No source available": "Nenhuma fonte disponível", "No sources found": "Nenhuma fonte encontrada", "No suggestion prompts": "Sem prompts sugeridos", @@ -1307,7 +1307,7 @@ "Only select users and groups with permission can access": "Somente usuários e grupos selecionados com permissão podem acessar.", "Only sync new/updated chats": "Sincronizar apenas conversas novas/atualizadas", "Oops! Looks like the URL is invalid. Please double-check and try again.": "Ops! Parece que a URL é inválida. Por favor, verifique novamente e tente de novo.", - "Oops! There are files still uploading. Please wait for the upload to complete.": "Ops! Existem arquivos a serem carregados. Por favor, aguarde que o carregamento tenha concluído.", + "Oops! There are files still uploading. Please wait for the upload to complete.": "Ops! Ainda há arquivos sendo enviados. Por favor, aguarde a conclusão do envio.", "Oops! There was an error in the previous response.": "Ops! Houve um erro na resposta anterior.", "Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "Ops! Você está usando um método não suportado (somente frontend). Por favor, sirva a WebUI a partir do backend.", "Open file": "Abrir arquivo", @@ -1316,7 +1316,7 @@ "Open modal to configure connection": "Abra o modal para configurar a conexão", "Open Modal To Manage Floating Quick Actions": "Abra o Modal para gerenciar ações rápidas flutuantes", "Open Modal To Manage Image Compression": "Abrir o Modal para gerenciar a compressão de imagens", - "Open Model Selector": "", + "Open Model Selector": "Abrir Seletor de Modelos", "Open Settings": "Abrir configurações", "Open Sidebar": "Abrir barra lateral", "Open User Profile Menu": "Abrir menu de perfil do usuário", @@ -1336,7 +1336,7 @@ "OpenAPI": "", "OpenAPI Spec": "", "openapi.json URL or Path": "", - "optional": "", + "optional": "opcional", "Optional": "Opcional", "or": "ou", "Ordered List": "Lista ordenada", @@ -1347,8 +1347,8 @@ "Output Format": "Formato de Saída", "Overview": "Visão Geral", "page": "página", - "Page": "", - "Page mode creates one document per page. Single mode combines all pages into one document for better chunking across page boundaries.": "", + "Page": "Página", + "Page mode creates one document per page. Single mode combines all pages into one document for better chunking across page boundaries.": "O modo de página cria um documento por página. O modo único combina todas as páginas em um único documento para melhor divisão entre páginas.", "Paginate": "Paginar", "Parameters": "Parâmetros", "Parent message not found": "Mensagem principal não encontrada", @@ -1358,7 +1358,7 @@ "Paste Large Text as File": "Cole Textos Longos como Arquivo", "PDF document (.pdf)": "Documento PDF (.pdf)", "PDF Extract Images (OCR)": "Extrair Imagens do PDF (OCR)", - "PDF Loader Mode": "", + "PDF Loader Mode": "Modo de carregamento de PDF", "pending": "pendente", "Pending": "Pendente", "Pending User Overlay Content": "Conteúdo de sobreposição de usuário pendente", @@ -1395,14 +1395,14 @@ "Please enter a valid ID": "Por favor, insira um ID válido", "Please enter a valid JSON spec": "Por favor, insira uma especificação JSON válida", "Please enter a valid path": "Por favor, insira um caminho válido", - "Please enter a valid URL": "Por favor, insira uma URL válido", + "Please enter a valid URL": "Por favor, insira uma URL válida", "Please enter a valid URL.": "Por favor, insira uma URL válida", "Please fill in all fields.": "Por favor, preencha todos os campos.", "Please register the OAuth client": "Por favor, registre o cliente OAuth", "Please save the connection to persist the OAuth client information and do not change the ID": "Salve a conexão para persistir as informações do cliente OAuth e não altere o ID", "Please select a model first.": "Selecione um modelo primeiro.", "Please select a model.": "Selecione um modelo.", - "Please select a reason": "Por favor, seleccione uma razão", + "Please select a reason": "Por favor, selecione um motivo", "Please select a valid JSON file": "Selecione um arquivo JSON válido", "Please select at least one user for Direct Message channel.": "Por favor, selecione pelo menos um usuário para o canal de Mensagens Diretas.", "Please wait until all files are uploaded.": "Aguarde até que todos os arquivos sejam enviados.", @@ -1418,18 +1418,18 @@ "Previous message": "Mensagem anterior", "Private": "Privado", "Private conversation between selected users": "Conversa privada entre usuários selecionados", - "Production version updated": "", + "Production version updated": "Versão de produção atualizada", "Profile": "Perfil", "Prompt": "", "Prompt Autocompletion": "Preenchimento automático de prompts", "Prompt Content": "Conteúdo do Prompt", "Prompt created successfully": "Prompt criado com sucesso", - "Prompt Name": "", + "Prompt Name": "Nome do prompt", "Prompt updated successfully": "Prompt atualizado com sucesso", "Prompts": "Prompts", - "Prompts Access": "Acessar prompts", + "Prompts Access": "Acesso aos Prompts", "Prompts Public Sharing": "Compartilhamento Público dos Prompts", - "Prompts Sharing": "Compatilhamento de Prompts", + "Prompts Sharing": "Compartilhamento de Prompts", "Provider Type": "Tipo de provedor", "Public": "Público", "Pull \"{{searchValue}}\" from Ollama.com": "Obter \"{{searchValue}}\" de Ollama.com", @@ -1449,16 +1449,16 @@ "Reason": "Razão", "Reasoning Effort": "Esforço de raciocínio", "Reasoning Tags": "Tags de raciocínio", - "Record": "Registro", + "Record": "Gravar", "Record voice": "Gravar voz", "Redirecting you to Open WebUI Community": "Redirecionando você para a Comunidade OpenWebUI", "Reduces the probability of generating nonsense. A higher value (e.g. 100) will give more diverse answers, while a lower value (e.g. 10) will be more conservative.": "Reduz a probabilidade de gerar respostas sem sentido. Um valor mais alto (por exemplo, 100) resultará em respostas mais diversas, enquanto um valor mais baixo (por exemplo, 10) será mais conservador.", "Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "Refira-se como \"Usuário\" (por exemplo, \"Usuário está aprendendo espanhol\")", - "Reference Chats": "Chats Anteriores", + "Reference Chats": "Chats de Referência", "Refused when it shouldn't have": "Recusado quando não deveria", "Regenerate": "Gerar novamente", "Regenerate Menu": "Regenerar Menu", - "Regenerate Response": "Refazer Resposta", + "Regenerate Response": "Regenerar Resposta", "Register Again": "Registre-se novamente", "Register Client": "Registrar cliente", "Registered": "Registrado", @@ -1496,7 +1496,7 @@ "Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "Notificações de resposta não podem ser ativadas pois as permissões do site foram negadas. Por favor, visite as configurações do seu navegador para conceder o acesso necessário.", "Response splitting": "Divisão da Resposta", "Response Watermark": "Marca d'água de resposta", - "Responses": "", + "Responses": "Respostas", "Result": "Resultado", "RESULT": "Resultado", "Retrieval": "Recuperação", @@ -1524,13 +1524,13 @@ "Search": "Pesquisar", "Search a model": "Pesquisar um modelo", "Search all emojis": "Pesquisar todos os emojis", - "Search and manage user memories": "", - "Search and view user chat history": "", + "Search and manage user memories": "Pesquisar e gerenciar memórias de usuários", + "Search and view user chat history": "Pesquise e visualize o histórico de chat do usuário", "Search Base": "Pesquisar Base", - "Search channels and channel messages": "", + "Search channels and channel messages": "Pesquisar canais e mensagens de canais", "Search Chats": "Pesquisar Chats", "Search Collection": "Pesquisar Coleção", - "Search Files": "", + "Search Files": "Pesquisar arquivos", "Search Filters": "Pesquisar Filtros", "search for archived chats": "pesquisar por chats arquivados", "search for folders": "procurar pastas", @@ -1543,13 +1543,13 @@ "Search Models": "Pesquisar Modelos", "Search Notes": "Pesquisar Notas", "Search options": "Opções de pesquisa", - "Search Prompts": "Prompts de Pesquisa", + "Search Prompts": "Pesquisar Prompts", "Search Result Count": "Contagem de Resultados da Pesquisa", - "Search Skills": "", + "Search Skills": "Pesquisar Habilidades", "Search the internet": "Pesquisar na Internet", - "Search the web and fetch URLs": "", + "Search the web and fetch URLs": "Pesquise na web e obtenha URLs", "Search Tools": "Pesquisar Ferramentas", - "Search, view, and manage user notes": "", + "Search, view, and manage user notes": "Pesquise, visualize e gerencie notas do usuário.", "SearchApi API Key": "Chave API SearchApi", "SearchApi Engine": "Motor SearchApi", "Searched {{count}} sites": "{{count}} sites pesquisados", @@ -1558,13 +1558,13 @@ "Searching Knowledge for \"{{searchQuery}}\"": "Buscando conhecimento para \"{{searchQuery}}\"", "Searching the web": "Pesquisando na Internet...", "Searxng Query URL": "URL da Consulta Searxng", - "Searxng search language (all, en, es, de, fr, etc.)": "Idioma de pesquisa de pesquisa (all, en, es, de, fr, etc.)", + "Searxng search language (all, en, es, de, fr, etc.)": "Idioma de pesquisa do Searxng (all, en, es, de, fr, etc.)", "See readme.md for instructions": "Veja readme.md para instruções", "See what's new": "Veja o que há de novo", "Seed": "Seed", "Select": "Selecionar", "Select a base model": "Selecione um modelo base", - "Select a base model (e.g. llama3, gpt-4o)": "Selecione um modelo básico (por exemplo, llama3, gpt-4o)", + "Select a base model (e.g. llama3, gpt-4o)": "Selecione um modelo base (por exemplo, llama3, gpt-4o)", "Select a conversation to preview": "Selecione uma conversa para visualizar", "Select a engine": "Selecione um motor", "Select a function": "Selecione uma função", @@ -1584,7 +1584,7 @@ "Select an embedding model engine": "Selecione um mecanismo de modelo de embedding", "Select an engine": "Selecione um motor", "Select an Ollama instance": "Selecione uma instância do Ollama", - "Select an option": "", + "Select an option": "Selecione uma opção", "Select an output format": "Selecione um formato de saída", "Select dtype": "Selecionar dtype", "Select Engine": "Selecionar Motor", @@ -1598,7 +1598,7 @@ "Send": "Enviar", "Send a Message": "Enviar uma Mensagem", "Send message": "Enviar mensagem", - "Send now": "", + "Send now": "Enviar agora", "Sends `stream_options: { include_usage: true }` in the request.\nSupported providers will return token usage information in the response when set.": "Envia `stream_options: { include_usage: true }` na solicitação. Provedores compatíveis retornarão informações sobre o uso de tokens na resposta quando configurado.", "September": "Setembro", "SerpApi API Key": "", @@ -1609,7 +1609,7 @@ "Server connection verified": "Conexão com o servidor verificada", "Session": "Sessão", "Set as default": "Definir como padrão", - "Set as Production": "", + "Set as Production": "Definir como Produção", "Set embedding model": "Definir modelo de embedding", "Set embedding model (e.g. {{model}})": "Definir modelo de embedding (por exemplo, {{model}})", "Set reranking model (e.g. {{model}})": "Definir modelo de reclassificação (por exemplo, {{model}})", @@ -1630,10 +1630,10 @@ "Settings saved successfully!": "Configurações salvas com sucesso!", "Share": "Compartilhar", "Share Chat": "Compartilhar Chat", - "Share link copied to clipboard.": "", + "Share link copied to clipboard.": "Link de compartilhamento copiado para a área de transferência.", "Share to Open WebUI Community": "Compartilhar com a Comunidade OpenWebUI", "Share your background and interests": "Fale sobre você e seus interesses", - "Shared Chats": "", + "Shared Chats": "Chats compartilhados", "Shared with you": "Compartilhado com você", "Sharing Permissions": "Permissões de compartilhamento", "Show": "Mostrar", @@ -1646,7 +1646,7 @@ "Show Shortcuts": "Mostrar Atalhos", "Show your support!": "Mostre seu apoio!", "Showcased creativity": "Criatividade exibida", - "Showing all messages (user + assistant) per user.": "", + "Showing all messages (user + assistant) per user.": "Exibindo todas as mensagens (usuário + assistente) por usuário.", "Sign in": "Entrar", "Sign in to {{WEBUI_NAME}}": "Faça login em {{WEBUI_NAME}}", "Sign in to {{WEBUI_NAME}} with LDAP": "Faça login em {{WEBUI_NAME}} com LDAP", @@ -1655,17 +1655,17 @@ "Sign up to {{WEBUI_NAME}}": "Inscreva-se em {{WEBUI_NAME}}", "Significantly improves accuracy by using an LLM to enhance tables, forms, inline math, and layout detection. Will increase latency. Defaults to False.": "Melhora significativamente a precisão usando um LLM para aprimorar tabelas, formulários, cálculos em linha e detecção de layout. Aumenta a latência. O padrão é Falso.", "Signing in to {{WEBUI_NAME}}": "Fazendo login em {{WEBUI_NAME}}", - "Single": "", + "Single": "Único", "Sink List": "", "sk-1234": "", - "Skill created successfully": "", - "Skill deleted successfully": "", - "Skill Description": "", - "Skill ID": "", - "Skill Name": "", - "Skill updated successfully": "", - "Skills": "", - "Skills Access": "", + "Skill created successfully": "Habilidade criada com sucesso", + "Skill deleted successfully": "Habilidade excluída com sucesso", + "Skill Description": "Descrição da habilidade", + "Skill ID": "ID da habilidade", + "Skill Name": "Nome da habilidade", + "Skill updated successfully": "Habilidade atualizada com sucesso", + "Skills": "Habilidades", + "Skills Access": "Acesso a habilidades", "Skip Cache": "Pular cache", "Skip the cache and re-run the inference. Defaults to False.": "Ignore o cache e execute a inferência novamente. O padrão é Falso.", "Something went wrong :/": "Algo deu errado :/", @@ -1694,7 +1694,7 @@ "Steps": "Passos", "Stop": "Parar", "Stop Download": "Parar download", - "Stop Generating": "Pare de gerar", + "Stop Generating": "Parar de Gerar", "Stop Sequence": "Sequência de Parada", "Stream Chat Response": "Stream Resposta do Chat", "Stream Delta Chunk Size": "", @@ -1705,7 +1705,7 @@ "STT Model": "Modelo STT", "STT Settings": "Configurações STT", "Stylized PDF Export": "Exportação de PDF estilizado", - "Subtitle": "Legenda", + "Subtitle": "Subtítulo", "Success": "Sucesso", "Successfully imported {{userCount}} users.": "{{userCount}} usuários importados com sucesso.", "Successfully updated.": "Atualizado com sucesso.", @@ -1749,21 +1749,21 @@ "The base to search for users": "Base para pesquisar usuários.", "The batch size determines how many text requests are processed together at once. A higher batch size can increase the performance and speed of the model, but it also requires more memory.": "O tamanho do lote determina quantas solicitações de texto são processadas simultaneamente. Um tamanho de lote maior pode aumentar o desempenho e a velocidade do modelo, mas também requer mais memória.", "The developers behind this plugin are passionate volunteers from the community. If you find this plugin helpful, please consider contributing to its development.": "Os desenvolvedores por trás deste plugin são voluntários apaixonados da comunidade. Se você achar este plugin útil, considere contribuir para o seu desenvolvimento.", - "The evaluation leaderboard is based on the Elo rating system and is updated in real-time.": "A evolução do ranking de avaliação é baseada no sistema Elo e será atualizada em tempo real.", + "The evaluation leaderboard is based on the Elo rating system and is updated in real-time.": "A tabela de classificação de avaliação é baseada no sistema Elo e é atualizada em tempo real.", "The format to return a response in. Format can be json or a JSON schema.": "O formato para retornar uma resposta. O formato pode ser json ou um esquema JSON.", "The height in pixels to compress images to. Leave empty for no compression.": "Altura em pixels para compactar as imagens. Deixe em branco para não compactar.", "The language of the input audio. Supplying the input language in ISO-639-1 (e.g. en) format will improve accuracy and latency. Leave blank to automatically detect the language.": "O idioma do áudio de entrada. Fornecer o idioma de entrada no formato ISO-639-1 (por exemplo, en) aumentará a precisão e a latência. Deixe em branco para detectar o idioma automaticamente.", "The LDAP attribute that maps to the mail that users use to sign in.": "O atributo LDAP que mapeia o e-mail que os usuários usam para fazer login.", "The LDAP attribute that maps to the username that users use to sign in.": "O atributo LDAP que mapeia para o nome de usuário que os usuários usam para fazer login.", - "The leaderboard is currently in beta, and we may adjust the rating calculations as we refine the algorithm.": "O ranking atual está em beta, e podemos ajustar as contas de avaliação como refinamos o algoritmo.", - "The maximum file size in MB. If the file size exceeds this limit, the file will not be uploaded.": "Máximo tamanho de arquivo em MB. Se o tamanho do arquivo exceder este limite, o arquivo não será enviado.", - "The maximum number of files that can be used at once in chat. If the number of files exceeds this limit, the files will not be uploaded.": "O número máximo de arquivos que podem ser utilizados a cada vez em chat. Se o número de arquivos exceder este limite, os arquivos não serão enviados.", + "The leaderboard is currently in beta, and we may adjust the rating calculations as we refine the algorithm.": "A tabela de classificação está atualmente em beta, e podemos ajustar os cálculos de avaliação conforme refinamos o algoritmo.", + "The maximum file size in MB. If the file size exceeds this limit, the file will not be uploaded.": "Tamanho máximo do arquivo em MB. Se o tamanho do arquivo exceder este limite, o arquivo não será enviado.", + "The maximum number of files that can be used at once in chat. If the number of files exceeds this limit, the files will not be uploaded.": "O número máximo de arquivos que podem ser utilizados de cada vez no chat. Se o número de arquivos exceder este limite, os arquivos não serão enviados.", "The output format for the text. Can be 'json', 'markdown', or 'html'. Defaults to 'markdown'.": "Formato de saída para o texto. Pode ser 'json', 'markdown' ou 'html'. O padrão é 'markdown'.", - "The passwords you entered don't quite match. Please double-check and try again.": "", + "The passwords you entered don't quite match. Please double-check and try again.": "As senhas digitadas não coincidem. Por favor, verifique e tente novamente.", "The score should be a value between 0.0 (0%) and 1.0 (100%).": "A pontuação deve ser um valor entre 0.0 (0%) e 1.0 (100%).", "The stream delta chunk size for the model. Increasing the chunk size will make the model respond with larger pieces of text at once.": "O tamanho do bloco delta do fluxo para o modelo. Aumentar o tamanho do bloco fará com que o modelo responda com trechos maiores de texto de uma só vez.", "The temperature of the model. Increasing the temperature will make the model answer more creatively.": "A temperatura do modelo. Aumentar a temperatura fará com que o modelo responda de forma mais criativa.", - "The Weight of BM25 Hybrid Search. 0 more semantic, 1 more lexical. Default 0.5": "O Peso da Busca Híbrida BM25. 0 a mais semântico, 1 a mais lexical. Padrão 0,5", + "The Weight of BM25 Hybrid Search. 0 more semantic, 1 more lexical. Default 0.5": "O Peso da Busca Híbrida BM25. 0 = mais semântico, 1 = mais lexical. Padrão 0,5", "The width in pixels to compress images to. Leave empty for no compression.": "A largura em pixels para compactar as imagens. Deixe em branco para não compactar.", "Theme": "Tema", "There was an error syncing your stats. Please try again.": "Ocorreu um erro ao sincronizar suas estatísticas. Tente novamente.", @@ -1772,7 +1772,7 @@ "This channel was created on {{createdAt}}. This is the very beginning of the {{channelName}} channel.": "Este canal foi criado em {{createdAt}}. Este é o início do canal {{channelName}}.", "This chat won't appear in history and your messages will not be saved.": "Este chat não aparecerá no histórico e suas mensagens não serão salvas.", "This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "Isso garante que suas conversas valiosas sejam salvas com segurança no banco de dados do backend. Obrigado!", - "This feature is currently experimental and may not work as expected.": "", + "This feature is currently experimental and may not work as expected.": "Esta funcionalidade encontra-se atualmente em fase experimental e poderá não funcionar conforme o esperado.", "This feature is experimental and may be modified or discontinued without notice.": "Este recurso é experimental e pode ser modificado ou descontinuado sem aviso prévio.", "This is a default user permission and will remain enabled.": "Esta é uma permissão de usuário padrão e permanecerá ativada.", "This is an experimental feature, it may not function as expected and is subject to change at any time.": "Esta é uma funcionalidade experimental, pode não funcionar como esperado e está sujeita a alterações a qualquer momento.", @@ -1791,13 +1791,13 @@ "Thorough explanation": "Explicação detalhada", "Thought for {{DURATION}}": "Pensado por {{DURATION}}", "Thought for {{DURATION}} seconds": "Pensado por {{DURATION}} segundos", - "Thought for less than a second": "Pensei por menos de um segundo", + "Thought for less than a second": "Pensou por menos de um segundo", "Thread": "Tópico", - "Thumbs up/down ratings from users on model responses": "", + "Thumbs up/down ratings from users on model responses": "Avaliações positivas/negativas dos usuários sobre as respostas do modelo", "Tika": "Tika", "Tika Server URL required.": "URL do servidor Tika necessária.", "Tiktoken": "", - "Time & Calculation": "", + "Time & Calculation": "Tempo e Cálculo", "Timeout": "", "Title": "Título", "Title Auto-Generation": "Geração Automática de Título", @@ -1810,7 +1810,7 @@ "To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "Para acessar a WebUI, entre em contato com o administrador. Os administradores podem gerenciar os status dos usuários no Painel de Administração.", "To attach knowledge base here, add them to the \"Knowledge\" workspace first.": "Para anexar a base de conhecimento aqui, adicione-os ao espaço de trabalho \"Conhecimento\" primeiro.", "To learn more about available endpoints, visit our documentation.": "Para saber mais sobre os endpoints disponíveis, visite nossa documentação.", - "To select skills here, add them to the \"Skills\" workspace first.": "", + "To select skills here, add them to the \"Skills\" workspace first.": "Para selecionar habilidades aqui, adicione-as primeiro ao espaço de trabalho \"Habilidades\".", "To select toolkits here, add them to the \"Tools\" workspace first.": "Para selecionar kits de ferramentas aqui, adicione-os ao espaço de trabalho \"Ferramentas\" primeiro.", "Toast notifications for new updates": "Notificações de alerta para novas atualizações", "Today": "Hoje", @@ -1818,7 +1818,7 @@ "Toggle Sidebar": "Alternar barra lateral", "Toggle whether current connection is active.": "Alterna se a conexão atual está ativa.", "Token": "", - "Token counts are estimates and may not reflect actual API usage": "", + "Token counts are estimates and may not reflect actual API usage": "A contagem de tokens é uma estimativa e pode não refletir o uso real da API.", "tokens": "", "Tokens": "", "Too verbose": "Muito detalhado", @@ -1831,7 +1831,7 @@ "Tool Servers": "Servidores de ferramentas", "Tool updated successfully": "Ferramenta atualizada com sucesso", "Tools": "Ferramentas", - "Tools Access": "Acesso as Ferramentas", + "Tools Access": "Acesso às Ferramentas", "Tools are a function calling system with arbitrary code execution": "Ferramentas são um sistema de chamada de funções com execução de código arbitrário", "Tools Function Calling Prompt": "Prompt de chamada de função de ferramentas", "Tools have a function calling system that allows arbitrary code execution.": "Ferramentas possuem um sistema de chamada de funções que permite a execução de código arbitrário.", @@ -1864,7 +1864,7 @@ "Unlock mysteries": "Desvendar mistérios", "Unpin": "Desfixar", "Unravel secrets": "Desvendar segredos", - "Unshare Chat": "", + "Unshare Chat": "Cancelar compartilhamento do chat", "Unsupported file type.": "Tipo de arquivo não suportado.", "Untagged": "Sem tag", "Untitled": "Sem título", @@ -1890,10 +1890,10 @@ "Uploaded files or images": "Arquivos ou imagens carregados", "Uploading file...": "Enviando arquivo...", "URL": "", - "URL is required": "URL é obrigratória", + "URL is required": "URL é obrigatória", "URL Mode": "Modo URL", "Usage": "Uso", - "Use": "", + "Use": "Usar", "Use '#' in the prompt input to load and include your knowledge.": "Usar '#' no prompt para carregar e incluir seus conhecimentos.", "Use /v1/chat/completions endpoint instead of /v1/audio/transcriptions for potentially better accuracy.": "Use o endpoint /v1/chat/completions em vez de /v1/audio/transcriptions para obter uma precisão potencialmente melhor.", "Use Chat Completions API": "", @@ -1903,7 +1903,7 @@ "Use proxy designated by http_proxy and https_proxy environment variables to fetch page contents.": "Use o proxy designado pelas variáveis de ambiente http_proxy e https_proxy para buscar o conteúdo da página.", "user": "usuário", "User": "Usuário", - "User Activity": "", + "User Activity": "Atividade do usuário", "User Groups": "Grupos de usuários", "User location successfully retrieved.": "Localização do usuário recuperada com sucesso.", "User menu": "Menu do usuário", @@ -1928,11 +1928,11 @@ "Verify SSL Certificate": "Verificar certificado SSL", "Version": "Versão", "Version {{selectedVersion}} of {{totalVersions}}": "Versão {{selectedVersion}} de {{totalVersions}}", - "Version deleted": "", + "Version deleted": "Versão excluída", "View Replies": "Ver respostas", "View Result from **{{NAME}}**": "Ver resultado de **{{NAME}}**", "Visibility": "Visibilidade", - "Visible": "", + "Visible": "Visível", "Visible to all users": "Visível para todos os usuários", "Vision": "Visão", "Voice": "Voz", @@ -1965,14 +1965,14 @@ "What is NOT shared:": "O que NÃO é compartilhado:", "What is shared:": "O que é compartilhado", "What's New in": "O que há de novo em", - "What's on your mind?": "O que você têm em mente?", + "What's on your mind?": "O que você tem em mente?", "When enabled, the model will respond to each chat message in real-time, generating a response as soon as the user sends a message. This mode is useful for live chat applications, but may impact performance on slower hardware.": "Quando habilitado, o modelo responderá a cada mensagem de chat em tempo real, gerando uma resposta assim que o usuário enviar uma mensagem. Este modo é útil para aplicativos de chat ao vivo, mas pode impactar o desempenho em hardware mais lento.", "wherever you are": "onde quer que você esteja.", "Whether to paginate the output. Each page will be separated by a horizontal rule and page number. Defaults to False.": "Se a saída deve ser paginada. Cada página será separada por uma régua horizontal e um número de página. O padrão é Falso.", "Whisper (Local)": "Whisper (Local)", "Who can share to this group": "Quem pode compartilhar neste grupo", "Why?": "Por que?", - "Widescreen Mode": "Modo Tela Cheia", + "Widescreen Mode": "Modo Widescreen", "Width": "Largura", "Wikipedia": "", "Won": "Ganhou", @@ -1980,7 +1980,7 @@ "Workspace": "Espaço de Trabalho", "Workspace Permissions": "Permissões do espaço de trabalho", "Write": "Escrever", - "Write a summary in 50 words that summarizes {{topic}}.": "Escreva um resumo em 50 palavras que resuma [tópico].", + "Write a summary in 50 words that summarizes {{topic}}.": "Escreva um resumo em 50 palavras que resuma {{topic}}.", "Write something...": "Escreva algo...", "Write your model system prompt content here\ne.g.) You are Mario from Super Mario Bros, acting as an assistant.": "Escreva aqui o conteúdo do system prompt do seu modelo\nex.: Você é o Mario do Super Mario Bros e atua como assistente.", "Yacy Instance URL": "", @@ -2000,15 +2000,15 @@ "You cannot upload an empty file.": "Você não pode carregar um arquivo vazio.", "You do not have permission to edit this model": "Você não tem permissão para editar este modelo.", "You do not have permission to edit this prompt.": "Você não tem permissão para editar este prompt.", - "You do not have permission to edit this skill.": "", - "You do not have permission to edit this tool": "", - "You do not have permission to make this public": "", + "You do not have permission to edit this skill.": "Você não tem permissão para editar esta habilidade.", + "You do not have permission to edit this tool": "Você não tem permissão para editar esta ferramenta.", + "You do not have permission to make this public": "Você não tem permissão para tornar isso público.", "You do not have permission to send messages in this channel.": "Você não tem permissão para enviar mensagens neste canal.", "You do not have permission to send messages in this thread.": "Você não tem permissão para enviar mensagens neste tópico.", "You do not have permission to upload files to this knowledge base.": "Você não tem permissão para enviar arquivos para esta base de conhecimento.", "You do not have permission to upload files.": "Você não tem permissão para fazer upload de arquivos.", "You have no archived conversations.": "Você não tem conversas arquivadas.", - "You have no shared conversations.": "", + "You have no shared conversations.": "Você não tem conversas compartilhadas.", "You have shared this chat": "Você compartilhou este chat", "You're a helpful assistant.": "Você é um assistente útil.", "You're now logged in.": "Você agora está logado.", From 0b05b2fc7ed4c38af158707438ff404d1beb7c91 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Fri, 13 Feb 2026 00:44:01 -0600 Subject: [PATCH 02/34] refac --- src/lib/components/chat/ModelSelector/Selector.svelte | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/lib/components/chat/ModelSelector/Selector.svelte b/src/lib/components/chat/ModelSelector/Selector.svelte index dce53f6408..d2ecde992f 100644 --- a/src/lib/components/chat/ModelSelector/Selector.svelte +++ b/src/lib/components/chat/ModelSelector/Selector.svelte @@ -178,6 +178,16 @@ selectedModelIdx = 0; } + // Set the virtual scroll position so the selected item is rendered and centered + const targetScrollTop = Math.max(0, selectedModelIdx * ITEM_HEIGHT - 128 + ITEM_HEIGHT / 2); + listScrollTop = targetScrollTop; + + await tick(); + + if (listContainer) { + listContainer.scrollTop = targetScrollTop; + } + await tick(); const item = document.querySelector(`[data-arrow-selected="true"]`); item?.scrollIntoView({ block: 'center', inline: 'nearest', behavior: 'instant' }); @@ -379,6 +389,7 @@ bind:open={show} onOpenChange={async () => { searchValue = ''; + listScrollTop = 0; window.setTimeout(() => document.getElementById('model-search-input')?.focus(), 0); resetView(); From 9886ebb97f1004ebcea764af84b4c78201b15c25 Mon Sep 17 00:00:00 2001 From: G30 <50341825+silentoplayz@users.noreply.github.com> Date: Fri, 13 Feb 2026 12:18:14 -0500 Subject: [PATCH 03/34] fix: resolve knowledge collection indentation/truncation issue by correcting flex layout (#21374) --- .../chat/MessageInput/InputMenu/Knowledge.svelte | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/lib/components/chat/MessageInput/InputMenu/Knowledge.svelte b/src/lib/components/chat/MessageInput/InputMenu/Knowledge.svelte index 249b2a2128..2df974c83d 100644 --- a/src/lib/components/chat/MessageInput/InputMenu/Knowledge.svelte +++ b/src/lib/components/chat/MessageInput/InputMenu/Knowledge.svelte @@ -186,12 +186,16 @@ }} data-selected={idx === selectedIdx} > -
+
- +
{decodeString(item?.name)}
From 4d5b7b3014c79b1705abff795da1618700ab0401 Mon Sep 17 00:00:00 2001 From: G30 <50341825+silentoplayz@users.noreply.github.com> Date: Fri, 13 Feb 2026 12:18:35 -0500 Subject: [PATCH 04/34] fix: resolve knowledge tooltip z-index issue in model edit page (#21375) --- .../Models/Knowledge/KnowledgeSelector.svelte | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/lib/components/workspace/Models/Knowledge/KnowledgeSelector.svelte b/src/lib/components/workspace/Models/Knowledge/KnowledgeSelector.svelte index 2d6eb0d114..23d8b9c58e 100644 --- a/src/lib/components/workspace/Models/Knowledge/KnowledgeSelector.svelte +++ b/src/lib/components/workspace/Models/Knowledge/KnowledgeSelector.svelte @@ -170,15 +170,27 @@ >
{#if item.type === 'note'} - + {:else if item.type === 'collection'} - + {:else if item.type === 'file'} - + {/if} @@ -186,6 +198,7 @@
{decodeString(item?.name)} From ddcec9842f195018f5ddbd0599f8667b35168019 Mon Sep 17 00:00:00 2001 From: G30 <50341825+silentoplayz@users.noreply.github.com> Date: Fri, 13 Feb 2026 12:18:58 -0500 Subject: [PATCH 05/34] fix: ensure sync modal only triggers if community sharing is enabled (#21376) --- src/routes/+layout.svelte | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 6c4d0e523c..66f66676f5 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -849,7 +849,11 @@ } // Auto-show SyncStatsModal when opened with ?sync=true (from community) - if ((window.opener ?? false) && $page.url.searchParams.get('sync') === 'true') { + if ( + (window.opener ?? false) && + $page.url.searchParams.get('sync') === 'true' && + ($config?.features?.enable_community_sharing ?? false) + ) { showSyncStatsModal = true; } From 7bda6bf767d5d5c4dc1111465096a88e10b5030e Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Fri, 13 Feb 2026 11:20:26 -0600 Subject: [PATCH 06/34] fix: PostgreSQL cannot use get_chat_ids_by_model_id Co-Authored-By: EntropyYue <164553692+EntropyYue@users.noreply.github.com> --- backend/open_webui/models/chat_messages.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/backend/open_webui/models/chat_messages.py b/backend/open_webui/models/chat_messages.py index fe3539f9cd..e6e932be12 100644 --- a/backend/open_webui/models/chat_messages.py +++ b/backend/open_webui/models/chat_messages.py @@ -15,6 +15,7 @@ Text, JSON, Index, + func, ) #################### @@ -279,25 +280,26 @@ def get_chat_ids_by_model_id( db: Optional[Session] = None, ) -> list[str]: """Get distinct chat_ids that used a specific model.""" - from sqlalchemy import distinct with get_db_context(db) as db: - query = db.query(distinct(ChatMessage.chat_id)).filter( - ChatMessage.model_id == model_id - ) + query = db.query( + ChatMessage.chat_id, + func.max(ChatMessage.created_at).label("last_message_at"), + ).filter(ChatMessage.model_id == model_id) if start_date: query = query.filter(ChatMessage.created_at >= start_date) if end_date: query = query.filter(ChatMessage.created_at <= end_date) - # Order by most recent message in each chat + # Group by chat_id and order by most recent message in each chat chat_ids = ( - query.order_by(ChatMessage.created_at.desc()) + query.group_by(ChatMessage.chat_id) + .order_by(func.max(ChatMessage.created_at).desc()) .offset(skip) .limit(limit) .all() ) - return [chat_id for (chat_id,) in chat_ids] + return [chat_id for chat_id, _ in chat_ids] def delete_messages_by_chat_id( self, chat_id: str, db: Optional[Session] = None From 73776d54b890271dbb4efb90f01ecb80867de32d Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Fri, 13 Feb 2026 18:22:32 +0100 Subject: [PATCH 07/34] fix: enforce public sharing permission checks across all resource types (#21358) The sharePublic prop in editor components (Knowledge, Tools, Skills, Prompts, Models) incorrectly included an "|| edit" / "|| write_access" condition, allowing users with write access to see and use the "Public" sharing option regardless of their actual public sharing permission. Additionally, all backend access/update endpoints only verified write authorization but did not check the corresponding sharing.public_* permission, allowing direct API calls to bypass frontend restrictions entirely. Frontend: removed the edit/write_access bypass from sharePublic in all five editor components so visibility is gated solely by the user's sharing.public_* permission or admin role. Backend: added has_public_read_access_grant checks to the access/update endpoints in knowledge.py, tools.py, prompts.py, skills.py, models.py, and notes.py. Public grants are silently stripped when the user lacks the corresponding permission. Fixes #21356 --- backend/open_webui/routers/knowledge.py | 19 +++++++++++++++++ backend/open_webui/routers/models.py | 21 ++++++++++++++++++- backend/open_webui/routers/notes.py | 18 ++++++++++++++++ backend/open_webui/routers/prompts.py | 21 ++++++++++++++++++- backend/open_webui/routers/skills.py | 21 ++++++++++++++++++- backend/open_webui/routers/tools.py | 21 ++++++++++++++++++- .../workspace/Knowledge/KnowledgeBase.svelte | 3 +-- .../workspace/Models/ModelEditor.svelte | 2 +- .../workspace/Prompts/PromptEditor.svelte | 2 +- .../workspace/Skills/SkillEditor.svelte | 2 +- .../workspace/Tools/ToolkitEditor.svelte | 2 +- 11 files changed, 122 insertions(+), 10 deletions(-) diff --git a/backend/open_webui/routers/knowledge.py b/backend/open_webui/routers/knowledge.py index eab00aa19b..e60c30cb62 100644 --- a/backend/open_webui/routers/knowledge.py +++ b/backend/open_webui/routers/knowledge.py @@ -511,6 +511,7 @@ class KnowledgeAccessGrantsForm(BaseModel): @router.post("/{id}/access/update", response_model=Optional[KnowledgeFilesResponse]) async def update_knowledge_access_by_id( + request: Request, id: str, form_data: KnowledgeAccessGrantsForm, user=Depends(get_verified_user), @@ -539,6 +540,24 @@ async def update_knowledge_access_by_id( detail=ERROR_MESSAGES.ACCESS_PROHIBITED, ) + # Strip public sharing if user lacks permission + if ( + user.role != "admin" + and has_public_read_access_grant(form_data.access_grants) + and not has_permission( + user.id, + "sharing.public_knowledge", + request.app.state.config.USER_PERMISSIONS, + ) + ): + form_data.access_grants = [ + g for g in form_data.access_grants + if not ( + g.get("principal_type") == "user" + and g.get("principal_id") == "*" + ) + ] + AccessGrants.set_access_grants("knowledge", id, form_data.access_grants, db=db) return KnowledgeFilesResponse( diff --git a/backend/open_webui/routers/models.py b/backend/open_webui/routers/models.py index 7202262bbd..666b7ced29 100644 --- a/backend/open_webui/routers/models.py +++ b/backend/open_webui/routers/models.py @@ -15,7 +15,7 @@ ModelAccessResponse, Models, ) -from open_webui.models.access_grants import AccessGrants +from open_webui.models.access_grants import AccessGrants, has_public_read_access_grant from pydantic import BaseModel from open_webui.constants import ERROR_MESSAGES @@ -506,6 +506,7 @@ class ModelAccessGrantsForm(BaseModel): @router.post("/model/access/update", response_model=Optional[ModelModel]) async def update_model_access_by_id( + request: Request, form_data: ModelAccessGrantsForm, user=Depends(get_verified_user), db: Session = Depends(get_session), @@ -533,6 +534,24 @@ async def update_model_access_by_id( detail=ERROR_MESSAGES.ACCESS_PROHIBITED, ) + # Strip public sharing if user lacks permission + if ( + user.role != "admin" + and has_public_read_access_grant(form_data.access_grants) + and not has_permission( + user.id, + "sharing.public_models", + request.app.state.config.USER_PERMISSIONS, + ) + ): + form_data.access_grants = [ + g for g in form_data.access_grants + if not ( + g.get("principal_type") == "user" + and g.get("principal_id") == "*" + ) + ] + AccessGrants.set_access_grants( "model", form_data.id, form_data.access_grants, db=db ) diff --git a/backend/open_webui/routers/notes.py b/backend/open_webui/routers/notes.py index 04841e87cc..0c4ca0d2e3 100644 --- a/backend/open_webui/routers/notes.py +++ b/backend/open_webui/routers/notes.py @@ -345,6 +345,24 @@ async def update_note_access_by_id( status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT() ) + # Strip public sharing if user lacks permission + if ( + user.role != "admin" + and has_public_read_access_grant(form_data.access_grants) + and not has_permission( + user.id, + "sharing.public_notes", + request.app.state.config.USER_PERMISSIONS, + ) + ): + form_data.access_grants = [ + g for g in form_data.access_grants + if not ( + g.get("principal_type") == "user" + and g.get("principal_id") == "*" + ) + ] + AccessGrants.set_access_grants("note", id, form_data.access_grants, db=db) return Notes.get_note_by_id(id, db=db) diff --git a/backend/open_webui/routers/prompts.py b/backend/open_webui/routers/prompts.py index e8d4660f03..8720ba1aa5 100644 --- a/backend/open_webui/routers/prompts.py +++ b/backend/open_webui/routers/prompts.py @@ -9,7 +9,7 @@ PromptModel, Prompts, ) -from open_webui.models.access_grants import AccessGrants +from open_webui.models.access_grants import AccessGrants, has_public_read_access_grant from open_webui.models.groups import Groups from open_webui.models.prompt_history import ( PromptHistories, @@ -436,6 +436,7 @@ class PromptAccessGrantsForm(BaseModel): @router.post("/id/{prompt_id}/access/update", response_model=Optional[PromptModel]) async def update_prompt_access_by_id( + request: Request, prompt_id: str, form_data: PromptAccessGrantsForm, user=Depends(get_verified_user), @@ -464,6 +465,24 @@ async def update_prompt_access_by_id( detail=ERROR_MESSAGES.ACCESS_PROHIBITED, ) + # Strip public sharing if user lacks permission + if ( + user.role != "admin" + and has_public_read_access_grant(form_data.access_grants) + and not has_permission( + user.id, + "sharing.public_prompts", + request.app.state.config.USER_PERMISSIONS, + ) + ): + form_data.access_grants = [ + g for g in form_data.access_grants + if not ( + g.get("principal_type") == "user" + and g.get("principal_id") == "*" + ) + ] + AccessGrants.set_access_grants("prompt", prompt_id, form_data.access_grants, db=db) return Prompts.get_prompt_by_id(prompt_id, db=db) diff --git a/backend/open_webui/routers/skills.py b/backend/open_webui/routers/skills.py index 367768e61b..a9a17f147a 100644 --- a/backend/open_webui/routers/skills.py +++ b/backend/open_webui/routers/skills.py @@ -17,7 +17,7 @@ SkillAccessListResponse, Skills, ) -from open_webui.models.access_grants import AccessGrants +from open_webui.models.access_grants import AccessGrants, has_public_read_access_grant from open_webui.utils.auth import get_admin_user, get_verified_user from open_webui.utils.access_control import has_access, has_permission @@ -312,6 +312,7 @@ class SkillAccessGrantsForm(BaseModel): @router.post("/id/{id}/access/update", response_model=Optional[SkillModel]) async def update_skill_access_by_id( + request: Request, id: str, form_data: SkillAccessGrantsForm, user=Depends(get_verified_user), @@ -340,6 +341,24 @@ async def update_skill_access_by_id( detail=ERROR_MESSAGES.UNAUTHORIZED, ) + # Strip public sharing if user lacks permission + if ( + user.role != "admin" + and has_public_read_access_grant(form_data.access_grants) + and not has_permission( + user.id, + "sharing.public_skills", + request.app.state.config.USER_PERMISSIONS, + ) + ): + form_data.access_grants = [ + g for g in form_data.access_grants + if not ( + g.get("principal_type") == "user" + and g.get("principal_id") == "*" + ) + ] + AccessGrants.set_access_grants("skill", id, form_data.access_grants, db=db) return Skills.get_skill_by_id(id, db=db) diff --git a/backend/open_webui/routers/tools.py b/backend/open_webui/routers/tools.py index 057eb509a1..81194c2f4d 100644 --- a/backend/open_webui/routers/tools.py +++ b/backend/open_webui/routers/tools.py @@ -21,7 +21,7 @@ ToolAccessResponse, Tools, ) -from open_webui.models.access_grants import AccessGrants +from open_webui.models.access_grants import AccessGrants, has_public_read_access_grant from open_webui.utils.plugin import ( load_tool_module_by_id, replace_imports, @@ -526,6 +526,7 @@ class ToolAccessGrantsForm(BaseModel): @router.post("/id/{id}/access/update", response_model=Optional[ToolModel]) async def update_tool_access_by_id( + request: Request, id: str, form_data: ToolAccessGrantsForm, user=Depends(get_verified_user), @@ -554,6 +555,24 @@ async def update_tool_access_by_id( detail=ERROR_MESSAGES.UNAUTHORIZED, ) + # Strip public sharing if user lacks permission + if ( + user.role != "admin" + and has_public_read_access_grant(form_data.access_grants) + and not has_permission( + user.id, + "sharing.public_tools", + request.app.state.config.USER_PERMISSIONS, + ) + ): + form_data.access_grants = [ + g for g in form_data.access_grants + if not ( + g.get("principal_type") == "user" + and g.get("principal_id") == "*" + ) + ] + AccessGrants.set_access_grants("tool", id, form_data.access_grants, db=db) return Tools.get_tool_by_id(id, db=db) diff --git a/src/lib/components/workspace/Knowledge/KnowledgeBase.svelte b/src/lib/components/workspace/Knowledge/KnowledgeBase.svelte index 50b4430332..58bb6203c5 100644 --- a/src/lib/components/workspace/Knowledge/KnowledgeBase.svelte +++ b/src/lib/components/workspace/Knowledge/KnowledgeBase.svelte @@ -837,8 +837,7 @@ bind:accessGrants={knowledge.access_grants} share={$user?.permissions?.sharing?.knowledge || $user?.role === 'admin'} sharePublic={$user?.permissions?.sharing?.public_knowledge || - $user?.role === 'admin' || - knowledge?.write_access} + $user?.role === 'admin'} onChange={async () => { try { await updateKnowledgeAccessGrants(localStorage.token, id, knowledge.access_grants ?? []); diff --git a/src/lib/components/workspace/Models/ModelEditor.svelte b/src/lib/components/workspace/Models/ModelEditor.svelte index 05d17ddaf6..80abd65445 100644 --- a/src/lib/components/workspace/Models/ModelEditor.svelte +++ b/src/lib/components/workspace/Models/ModelEditor.svelte @@ -342,7 +342,7 @@ bind:accessGrants accessRoles={preset ? ['read', 'write'] : ['read']} share={$user?.permissions?.sharing?.models || $user?.role === 'admin'} - sharePublic={$user?.permissions?.sharing?.public_models || $user?.role === 'admin' || edit} + sharePublic={$user?.permissions?.sharing?.public_models || $user?.role === 'admin'} onChange={async () => { if (edit && model?.id) { try { diff --git a/src/lib/components/workspace/Prompts/PromptEditor.svelte b/src/lib/components/workspace/Prompts/PromptEditor.svelte index 288d674c0d..118575d1f4 100644 --- a/src/lib/components/workspace/Prompts/PromptEditor.svelte +++ b/src/lib/components/workspace/Prompts/PromptEditor.svelte @@ -282,7 +282,7 @@ bind:accessGrants accessRoles={['read', 'write']} share={$user?.permissions?.sharing?.prompts || $user?.role === 'admin'} - sharePublic={$user?.permissions?.sharing?.public_prompts || $user?.role === 'admin' || edit} + sharePublic={$user?.permissions?.sharing?.public_prompts || $user?.role === 'admin'} onChange={async () => { if (edit && prompt?.id) { try { diff --git a/src/lib/components/workspace/Skills/SkillEditor.svelte b/src/lib/components/workspace/Skills/SkillEditor.svelte index 1e63ff0c08..2670337b00 100644 --- a/src/lib/components/workspace/Skills/SkillEditor.svelte +++ b/src/lib/components/workspace/Skills/SkillEditor.svelte @@ -113,7 +113,7 @@ bind:accessGrants accessRoles={['read', 'write']} share={$user?.permissions?.sharing?.skills || $user?.role === 'admin'} - sharePublic={$user?.permissions?.sharing?.public_skills || $user?.role === 'admin' || edit} + sharePublic={$user?.permissions?.sharing?.public_skills || $user?.role === 'admin'} onChange={async () => { if (edit && skill?.id) { try { diff --git a/src/lib/components/workspace/Tools/ToolkitEditor.svelte b/src/lib/components/workspace/Tools/ToolkitEditor.svelte index 4822ebbf8c..c73d47a793 100644 --- a/src/lib/components/workspace/Tools/ToolkitEditor.svelte +++ b/src/lib/components/workspace/Tools/ToolkitEditor.svelte @@ -192,7 +192,7 @@ class Tools: bind:accessGrants accessRoles={['read', 'write']} share={$user?.permissions?.sharing?.tools || $user?.role === 'admin'} - sharePublic={$user?.permissions?.sharing?.public_tools || $user?.role === 'admin' || edit} + sharePublic={$user?.permissions?.sharing?.public_tools || $user?.role === 'admin'} onChange={async () => { if (edit && id) { try { From b4c3f54f9648c4232a0fd6557703ffa66fcf4caa Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Fri, 13 Feb 2026 11:24:08 -0600 Subject: [PATCH 08/34] fix: skills postgres issue --- backend/open_webui/models/skills.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/open_webui/models/skills.py b/backend/open_webui/models/skills.py index 71e8f97b31..1262830153 100644 --- a/backend/open_webui/models/skills.py +++ b/backend/open_webui/models/skills.py @@ -3,13 +3,13 @@ from typing import Optional from sqlalchemy.orm import Session -from open_webui.internal.db import Base, JSONField, get_db, get_db_context +from open_webui.internal.db import Base, get_db, get_db_context from open_webui.models.users import Users, UserResponse from open_webui.models.groups import Groups from open_webui.models.access_grants import AccessGrantModel, AccessGrants from pydantic import BaseModel, ConfigDict, Field -from sqlalchemy import BigInteger, Boolean, Column, String, Text, or_ +from sqlalchemy import JSON, BigInteger, Boolean, Column, String, Text, or_ log = logging.getLogger(__name__) @@ -26,7 +26,7 @@ class Skill(Base): name = Column(Text, unique=True) description = Column(Text, nullable=True) content = Column(Text) - meta = Column(JSONField) + meta = Column(JSON) is_active = Column(Boolean, default=True) updated_at = Column(BigInteger) From df6e38039f14d45764142b9d60082bb815e11d57 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Fri, 13 Feb 2026 13:29:22 -0600 Subject: [PATCH 09/34] refac --- CHANGELOG.md | 12 ++++++++++++ backend/open_webui/routers/knowledge.py | 8 +++----- backend/open_webui/routers/models.py | 8 +++----- backend/open_webui/routers/notes.py | 8 +++----- backend/open_webui/routers/ollama.py | 19 ------------------- backend/open_webui/routers/prompts.py | 8 +++----- backend/open_webui/routers/skills.py | 8 +++----- backend/open_webui/routers/tools.py | 8 +++----- package-lock.json | 4 ++-- package.json | 2 +- .../workspace/Knowledge/KnowledgeBase.svelte | 3 +-- 11 files changed, 34 insertions(+), 54 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 955c1f066e..ff30a13565 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.8.1] - 2026-02-13 + +### Fixed + +- 🔒 **Public sharing permission bypass fix.** Users with write access to knowledge bases, tools, skills, prompts, and models could previously see and use the "Public" sharing option regardless of their actual sharing permissions, and direct API calls could bypass frontend restrictions entirely; both frontend and backend now properly enforce `sharing.public_*` permissions, silently stripping public grants when users lack the corresponding permission. [#21356](https://github.com/open-webui/open-webui/issues/21356), [#21358](https://github.com/open-webui/open-webui/pull/21358) +- 🐘 **PostgreSQL analytics query fix.** The `get_chat_ids_by_model_id` function now works correctly with PostgreSQL by using `GROUP BY` with aggregate ordering instead of `DISTINCT` with non-aggregate `ORDER BY`, which PostgreSQL does not support. [Commit](https://github.com/open-webui/open-webui/commit/7bda6bf767d5d5c4dc1111465096a88e10b5030e) +- 🐘 **Skills PostgreSQL compatibility fix.** Skills now work correctly with PostgreSQL by using SQLAlchemy's native `JSON` column type instead of the custom `JSONField`, which caused compatibility issues. [Commit](https://github.com/open-webui/open-webui/commit/b4c3f54f9648c4232a0fd6557703ffa66fcf4caa) +- 🔍 **Knowledge tooltip z-index fix.** Knowledge base tooltips in the model editor no longer render behind other UI elements. [#21375](https://github.com/open-webui/open-webui/pull/21375) +- 📚 **Knowledge collection layout fix.** Knowledge collection names in the chat input menu no longer appear indented or truncated due to incorrect flex layout. [#21374](https://github.com/open-webui/open-webui/pull/21374) +- 🎯 **Model selector scroll position fix.** The model selector dropdown now correctly scrolls to and centers the currently selected model when opened, and resets scroll position when reopened. [Commit](https://github.com/open-webui/open-webui/commit/0b05b2fc7ed4c38af158707438ff404d1beb7c91) +- 🌐 **Translation updates.** Portuguese (Brazil) translations were updated. [#21345](https://github.com/open-webui/open-webui/pull/21345) + ## [0.8.0] - 2026-02-12 ### Added diff --git a/backend/open_webui/routers/knowledge.py b/backend/open_webui/routers/knowledge.py index e60c30cb62..d4b1e0a803 100644 --- a/backend/open_webui/routers/knowledge.py +++ b/backend/open_webui/routers/knowledge.py @@ -551,11 +551,9 @@ async def update_knowledge_access_by_id( ) ): form_data.access_grants = [ - g for g in form_data.access_grants - if not ( - g.get("principal_type") == "user" - and g.get("principal_id") == "*" - ) + grant + for grant in form_data.access_grants + if not (grant.get("principal_type") == "user" and grant.get("principal_id") == "*") ] AccessGrants.set_access_grants("knowledge", id, form_data.access_grants, db=db) diff --git a/backend/open_webui/routers/models.py b/backend/open_webui/routers/models.py index 666b7ced29..4befd4c200 100644 --- a/backend/open_webui/routers/models.py +++ b/backend/open_webui/routers/models.py @@ -545,11 +545,9 @@ async def update_model_access_by_id( ) ): form_data.access_grants = [ - g for g in form_data.access_grants - if not ( - g.get("principal_type") == "user" - and g.get("principal_id") == "*" - ) + grant + for grant in form_data.access_grants + if not (grant.get("principal_type") == "user" and grant.get("principal_id") == "*") ] AccessGrants.set_access_grants( diff --git a/backend/open_webui/routers/notes.py b/backend/open_webui/routers/notes.py index 0c4ca0d2e3..cba4c3f4ca 100644 --- a/backend/open_webui/routers/notes.py +++ b/backend/open_webui/routers/notes.py @@ -356,11 +356,9 @@ async def update_note_access_by_id( ) ): form_data.access_grants = [ - g for g in form_data.access_grants - if not ( - g.get("principal_type") == "user" - and g.get("principal_id") == "*" - ) + grant + for grant in form_data.access_grants + if not (grant.get("principal_type") == "user" and grant.get("principal_id") == "*") ] AccessGrants.set_access_grants("note", id, form_data.access_grants, db=db) diff --git a/backend/open_webui/routers/ollama.py b/backend/open_webui/routers/ollama.py index fea2497561..7356a2b0ba 100644 --- a/backend/open_webui/routers/ollama.py +++ b/backend/open_webui/routers/ollama.py @@ -653,10 +653,6 @@ async def unload_model( await get_all_models(request, user=user) models = request.app.state.OLLAMA_MODELS - # Canonicalize model name (if not supplied with version) - if ":" not in model_name: - model_name = f"{model_name}:latest" - if model_name not in models: raise HTTPException( status_code=400, detail=ERROR_MESSAGES.MODEL_NOT_FOUND(model_name) @@ -1353,9 +1349,6 @@ async def generate_chat_completion( detail="Model not found", ) - if ":" not in payload["model"]: - payload["model"] = f"{payload['model']}:latest" - url, url_idx = await get_ollama_url(request, payload["model"], url_idx) api_config = request.app.state.config.OLLAMA_API_CONFIGS.get( str(url_idx), @@ -1432,9 +1425,6 @@ async def generate_openai_completion( del payload["metadata"] model_id = form_data.model - if ":" not in model_id: - model_id = f"{model_id}:latest" - model_info = Models.get_model_by_id(model_id) if model_info: if model_info.base_model_id: @@ -1466,9 +1456,6 @@ async def generate_openai_completion( detail="Model not found", ) - if ":" not in payload["model"]: - payload["model"] = f"{payload['model']}:latest" - url, url_idx = await get_ollama_url(request, payload["model"], url_idx) api_config = request.app.state.config.OLLAMA_API_CONFIGS.get( str(url_idx), @@ -1518,9 +1505,6 @@ async def generate_openai_chat_completion( del payload["metadata"] model_id = completion_form.model - if ":" not in model_id: - model_id = f"{model_id}:latest" - model_info = Models.get_model_by_id(model_id) if model_info: if model_info.base_model_id: @@ -1556,9 +1540,6 @@ async def generate_openai_chat_completion( detail="Model not found", ) - if ":" not in payload["model"]: - payload["model"] = f"{payload['model']}:latest" - url, url_idx = await get_ollama_url(request, payload["model"], url_idx) api_config = request.app.state.config.OLLAMA_API_CONFIGS.get( str(url_idx), diff --git a/backend/open_webui/routers/prompts.py b/backend/open_webui/routers/prompts.py index 8720ba1aa5..4d0bd07d0a 100644 --- a/backend/open_webui/routers/prompts.py +++ b/backend/open_webui/routers/prompts.py @@ -476,11 +476,9 @@ async def update_prompt_access_by_id( ) ): form_data.access_grants = [ - g for g in form_data.access_grants - if not ( - g.get("principal_type") == "user" - and g.get("principal_id") == "*" - ) + grant + for grant in form_data.access_grants + if not (grant.get("principal_type") == "user" and grant.get("principal_id") == "*") ] AccessGrants.set_access_grants("prompt", prompt_id, form_data.access_grants, db=db) diff --git a/backend/open_webui/routers/skills.py b/backend/open_webui/routers/skills.py index a9a17f147a..2a51b993c8 100644 --- a/backend/open_webui/routers/skills.py +++ b/backend/open_webui/routers/skills.py @@ -352,11 +352,9 @@ async def update_skill_access_by_id( ) ): form_data.access_grants = [ - g for g in form_data.access_grants - if not ( - g.get("principal_type") == "user" - and g.get("principal_id") == "*" - ) + grant + for grant in form_data.access_grants + if not (grant.get("principal_type") == "user" and grant.get("principal_id") == "*") ] AccessGrants.set_access_grants("skill", id, form_data.access_grants, db=db) diff --git a/backend/open_webui/routers/tools.py b/backend/open_webui/routers/tools.py index 81194c2f4d..60fecbb6fc 100644 --- a/backend/open_webui/routers/tools.py +++ b/backend/open_webui/routers/tools.py @@ -566,11 +566,9 @@ async def update_tool_access_by_id( ) ): form_data.access_grants = [ - g for g in form_data.access_grants - if not ( - g.get("principal_type") == "user" - and g.get("principal_id") == "*" - ) + grant + for grant in form_data.access_grants + if not (grant.get("principal_type") == "user" and grant.get("principal_id") == "*") ] AccessGrants.set_access_grants("tool", id, form_data.access_grants, db=db) diff --git a/package-lock.json b/package-lock.json index 0a85159d06..b837771419 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "open-webui", - "version": "0.8.0", + "version": "0.8.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "open-webui", - "version": "0.8.0", + "version": "0.8.1", "dependencies": { "@azure/msal-browser": "^4.5.0", "@codemirror/lang-javascript": "^6.2.2", diff --git a/package.json b/package.json index 673bad49cc..a6e16abfa6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "open-webui", - "version": "0.8.0", + "version": "0.8.1", "private": true, "scripts": { "dev": "npm run pyodide:fetch && vite dev --host", diff --git a/src/lib/components/workspace/Knowledge/KnowledgeBase.svelte b/src/lib/components/workspace/Knowledge/KnowledgeBase.svelte index 58bb6203c5..4783530777 100644 --- a/src/lib/components/workspace/Knowledge/KnowledgeBase.svelte +++ b/src/lib/components/workspace/Knowledge/KnowledgeBase.svelte @@ -836,8 +836,7 @@ bind:show={showAccessControlModal} bind:accessGrants={knowledge.access_grants} share={$user?.permissions?.sharing?.knowledge || $user?.role === 'admin'} - sharePublic={$user?.permissions?.sharing?.public_knowledge || - $user?.role === 'admin'} + sharePublic={$user?.permissions?.sharing?.public_knowledge || $user?.role === 'admin'} onChange={async () => { try { await updateKnowledgeAccessGrants(localStorage.token, id, knowledge.access_grants ?? []); From d01b1d48809c105b31a972d47b47808dabb98999 Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Fri, 13 Feb 2026 20:32:48 +0100 Subject: [PATCH 10/34] enh: apply admin default to builtin web search (#21373) --- backend/open_webui/tools/builtin.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/backend/open_webui/tools/builtin.py b/backend/open_webui/tools/builtin.py index 0175ba5583..cec0375ab7 100644 --- a/backend/open_webui/tools/builtin.py +++ b/backend/open_webui/tools/builtin.py @@ -167,6 +167,9 @@ async def search_web( engine = __request__.app.state.config.WEB_SEARCH_ENGINE user = UserModel(**__user__) if __user__ else None + # Use admin-configured result count if configured, falling back to model-provided count of provided, else default to 5 + count = __request__.app.state.config.WEB_SEARCH_RESULT_COUNT or count + results = await asyncio.to_thread(_search_web, __request__, engine, query, user) # Limit results From 97a3b1528de7689fc8fa9bd90ac017ce384abc63 Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Fri, 13 Feb 2026 20:37:12 +0100 Subject: [PATCH 11/34] Update utils.py (#21105) --- backend/open_webui/retrieval/vector/utils.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/backend/open_webui/retrieval/vector/utils.py b/backend/open_webui/retrieval/vector/utils.py index a597390b92..a39d364419 100644 --- a/backend/open_webui/retrieval/vector/utils.py +++ b/backend/open_webui/retrieval/vector/utils.py @@ -4,6 +4,7 @@ def filter_metadata(metadata: dict[str, any]) -> dict[str, any]: + # Removes large/redundant fields from metadata dict. metadata = { key: value for key, value in metadata.items() if key not in KEYS_TO_EXCLUDE } @@ -13,16 +14,15 @@ def filter_metadata(metadata: dict[str, any]) -> dict[str, any]: def process_metadata( metadata: dict[str, any], ) -> dict[str, any]: + # Removes large fields and converts non-serializable types (datetime, list, dict) to strings. + result = {} for key, value in metadata.items(): - # Remove large fields + # Skip large fields if key in KEYS_TO_EXCLUDE: - del metadata[key] - + continue # Convert non-serializable fields to strings - if ( - isinstance(value, datetime) - or isinstance(value, list) - or isinstance(value, dict) - ): - metadata[key] = str(value) - return metadata + if isinstance(value, (datetime, list, dict)): + result[key] = str(value) + else: + result[key] = value + return result From ca6b18ab5cb94153a9dae233f975d36bf6b19b76 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Fri, 13 Feb 2026 13:40:59 -0600 Subject: [PATCH 12/34] refac: is_user_active --- backend/open_webui/models/users.py | 8 ++++++++ backend/open_webui/routers/channels.py | 8 ++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/backend/open_webui/models/users.py b/backend/open_webui/models/users.py index 7e398c1e4a..fffeb32d3e 100644 --- a/backend/open_webui/models/users.py +++ b/backend/open_webui/models/users.py @@ -762,6 +762,14 @@ def get_active_user_count(self, db: Optional[Session] = None) -> int: ) return count + @staticmethod + def is_active(user: UserModel) -> bool: + """Compute active status from an already-loaded UserModel (no DB hit).""" + if user.last_active_at: + three_minutes_ago = int(time.time()) - 180 + return user.last_active_at >= three_minutes_ago + return False + def is_user_active(self, user_id: str, db: Optional[Session] = None) -> bool: with get_db_context(db) as db: user = db.query(User).filter_by(id=user_id).first() diff --git a/backend/open_webui/routers/channels.py b/backend/open_webui/routers/channels.py index 1748eaf7ea..3add5023e7 100644 --- a/backend/open_webui/routers/channels.py +++ b/backend/open_webui/routers/channels.py @@ -204,7 +204,7 @@ async def get_channels( UserIdNameStatusResponse( **{ **user.model_dump(), - "is_active": Users.is_user_active(user.id, db=db), + "is_active": Users.is_active(user), } ) for user in Users.get_users_by_user_ids(user_ids, db=db) @@ -424,7 +424,7 @@ async def get_channel_by_id( UserIdNameStatusResponse( **{ **user.model_dump(), - "is_active": Users.is_user_active(user.id, db=db), + "is_active": Users.is_active(user), } ) for user in Users.get_users_by_user_ids(user_ids, db=db) @@ -541,7 +541,7 @@ async def get_channel_members_by_id( return { "users": [ UserModelResponse( - **user.model_dump(), is_active=Users.is_user_active(user.id, db=db) + **user.model_dump(), is_active=Users.is_active(user) ) for user in users ], @@ -576,7 +576,7 @@ async def get_channel_members_by_id( return { "users": [ UserModelResponse( - **user.model_dump(), is_active=Users.is_user_active(user.id, db=db) + **user.model_dump(), is_active=Users.is_active(user) ) for user in users ], From 20de5a87da0c12e4052b50887a42ddd7228c5ef5 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Fri, 13 Feb 2026 13:43:43 -0600 Subject: [PATCH 13/34] refac --- backend/open_webui/routers/models.py | 7 ++++++- backend/open_webui/routers/prompts.py | 6 +++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/backend/open_webui/routers/models.py b/backend/open_webui/routers/models.py index 4befd4c200..43065faa13 100644 --- a/backend/open_webui/routers/models.py +++ b/backend/open_webui/routers/models.py @@ -84,14 +84,18 @@ async def get_models( if direction: filter["direction"] = direction + # Pre-fetch user group IDs once - used for both filter and write_access check + groups = Groups.get_groups_by_member_id(user.id, db=db) + user_group_ids = {group.id for group in groups} + if not user.role == "admin" or not BYPASS_ADMIN_ACCESS_CONTROL: - groups = Groups.get_groups_by_member_id(user.id, db=db) if groups: filter["group_ids"] = [group.id for group in groups] filter["user_id"] = user.id result = Models.search_models(user.id, filter=filter, skip=skip, limit=limit, db=db) + return ModelAccessListResponse( items=[ ModelAccessResponse( @@ -104,6 +108,7 @@ async def get_models( resource_type="model", resource_id=model.id, permission="write", + user_group_ids=user_group_ids, db=db, ) ), diff --git a/backend/open_webui/routers/prompts.py b/backend/open_webui/routers/prompts.py index 4d0bd07d0a..86d2648a88 100644 --- a/backend/open_webui/routers/prompts.py +++ b/backend/open_webui/routers/prompts.py @@ -100,8 +100,11 @@ async def get_prompt_list( if direction: filter["direction"] = direction + # Pre-fetch user group IDs once - used for both filter and write_access check + groups = Groups.get_groups_by_member_id(user.id, db=db) + user_group_ids = {group.id for group in groups} + if not (user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL): - groups = Groups.get_groups_by_member_id(user.id, db=db) if groups: filter["group_ids"] = [group.id for group in groups] @@ -123,6 +126,7 @@ async def get_prompt_list( resource_type="prompt", resource_id=prompt.id, permission="write", + user_group_ids=user_group_ids, db=db, ) ), From 589c4e64c1b7bb7a7a5abc20382b92fb860e28c2 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Fri, 13 Feb 2026 13:56:29 -0600 Subject: [PATCH 14/34] refac --- backend/open_webui/models/access_grants.py | 56 +++++++++++++++++++++ backend/open_webui/routers/knowledge.py | 46 +++++++++++------ backend/open_webui/routers/models.py | 20 +++++--- backend/open_webui/routers/ollama.py | 58 ++++++++++++++-------- backend/open_webui/routers/openai.py | 27 ++++++---- backend/open_webui/routers/prompts.py | 20 +++++--- backend/open_webui/utils/models.py | 22 ++++---- 7 files changed, 178 insertions(+), 71 deletions(-) diff --git a/backend/open_webui/models/access_grants.py b/backend/open_webui/models/access_grants.py index fa6e79a8db..dd5a344b46 100644 --- a/backend/open_webui/models/access_grants.py +++ b/backend/open_webui/models/access_grants.py @@ -515,6 +515,62 @@ def has_access( ) return exists is not None + def get_accessible_resource_ids( + self, + user_id: str, + resource_type: str, + resource_ids: list[str], + permission: str = "read", + user_group_ids: Optional[set[str]] = None, + db: Optional[Session] = None, + ) -> set[str]: + """ + Batch check: return the subset of resource_ids that the user can access. + + This replaces calling has_access() in a loop (N+1) with a single query. + """ + if not resource_ids: + return set() + + with get_db_context(db) as db: + conditions = [ + and_( + AccessGrant.principal_type == "user", + AccessGrant.principal_id == "*", + ), + and_( + AccessGrant.principal_type == "user", + AccessGrant.principal_id == user_id, + ), + ] + + if user_group_ids is None: + from open_webui.models.groups import Groups + + user_groups = Groups.get_groups_by_member_id(user_id, db=db) + user_group_ids = {group.id for group in user_groups} + + if user_group_ids: + conditions.append( + and_( + AccessGrant.principal_type == "group", + AccessGrant.principal_id.in_(user_group_ids), + ) + ) + + rows = ( + db.query(AccessGrant.resource_id) + .filter( + AccessGrant.resource_type == resource_type, + AccessGrant.resource_id.in_(resource_ids), + AccessGrant.permission == permission, + or_(*conditions), + ) + .distinct() + .all() + ) + return {row[0] for row in rows} + def get_users_with_access( self, resource_type: str, diff --git a/backend/open_webui/routers/knowledge.py b/backend/open_webui/routers/knowledge.py index d4b1e0a803..d620c1745f 100644 --- a/backend/open_webui/routers/knowledge.py +++ b/backend/open_webui/routers/knowledge.py @@ -115,8 +115,10 @@ async def get_knowledge_bases( skip = (page - 1) * limit filter = {} + groups = Groups.get_groups_by_member_id(user.id, db=db) + user_group_ids = {group.id for group in groups} + if not user.role == "admin" or not BYPASS_ADMIN_ACCESS_CONTROL: - groups = Groups.get_groups_by_member_id(user.id, db=db) if groups: filter["group_ids"] = [group.id for group in groups] @@ -126,6 +128,17 @@ async def get_knowledge_bases( user.id, filter=filter, skip=skip, limit=limit, db=db ) + # Batch-fetch writable knowledge IDs in a single query instead of N has_access calls + knowledge_base_ids = [knowledge_base.id for knowledge_base in result.items] + writable_knowledge_base_ids = AccessGrants.get_accessible_resource_ids( + user_id=user.id, + resource_type="knowledge", + resource_ids=knowledge_base_ids, + permission="write", + user_group_ids=user_group_ids, + db=db, + ) + return KnowledgeAccessListResponse( items=[ KnowledgeAccessResponse( @@ -133,13 +146,7 @@ async def get_knowledge_bases( write_access=( user.id == knowledge_base.user_id or (user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL) - or AccessGrants.has_access( - user_id=user.id, - resource_type="knowledge", - resource_id=knowledge_base.id, - permission="write", - db=db, - ) + or knowledge_base.id in writable_knowledge_base_ids ), ) for knowledge_base in result.items @@ -166,8 +173,10 @@ async def search_knowledge_bases( if view_option: filter["view_option"] = view_option + groups = Groups.get_groups_by_member_id(user.id, db=db) + user_group_ids = {group.id for group in groups} + if not user.role == "admin" or not BYPASS_ADMIN_ACCESS_CONTROL: - groups = Groups.get_groups_by_member_id(user.id, db=db) if groups: filter["group_ids"] = [group.id for group in groups] @@ -177,6 +186,17 @@ async def search_knowledge_bases( user.id, filter=filter, skip=skip, limit=limit, db=db ) + # Batch-fetch writable knowledge IDs in a single query instead of N has_access calls + knowledge_base_ids = [knowledge_base.id for knowledge_base in result.items] + writable_knowledge_base_ids = AccessGrants.get_accessible_resource_ids( + user_id=user.id, + resource_type="knowledge", + resource_ids=knowledge_base_ids, + permission="write", + user_group_ids=user_group_ids, + db=db, + ) + return KnowledgeAccessListResponse( items=[ KnowledgeAccessResponse( @@ -184,13 +204,7 @@ async def search_knowledge_bases( write_access=( user.id == knowledge_base.user_id or (user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL) - or AccessGrants.has_access( - user_id=user.id, - resource_type="knowledge", - resource_id=knowledge_base.id, - permission="write", - db=db, - ) + or knowledge_base.id in writable_knowledge_base_ids ), ) for knowledge_base in result.items diff --git a/backend/open_webui/routers/models.py b/backend/open_webui/routers/models.py index 43065faa13..fe4137cab9 100644 --- a/backend/open_webui/routers/models.py +++ b/backend/open_webui/routers/models.py @@ -96,6 +96,17 @@ async def get_models( result = Models.search_models(user.id, filter=filter, skip=skip, limit=limit, db=db) + # Batch-fetch writable model IDs in a single query instead of N has_access calls + model_ids = [model.id for model in result.items] + writable_model_ids = AccessGrants.get_accessible_resource_ids( + user_id=user.id, + resource_type="model", + resource_ids=model_ids, + permission="write", + user_group_ids=user_group_ids, + db=db, + ) + return ModelAccessListResponse( items=[ ModelAccessResponse( @@ -103,14 +114,7 @@ async def get_models( write_access=( (user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL) or user.id == model.user_id - or AccessGrants.has_access( - user_id=user.id, - resource_type="model", - resource_id=model.id, - permission="write", - user_group_ids=user_group_ids, - db=db, - ) + or model.id in writable_model_ids ), ) for model in result.items diff --git a/backend/open_webui/routers/ollama.py b/backend/open_webui/routers/ollama.py index 7356a2b0ba..394735e898 100644 --- a/backend/open_webui/routers/ollama.py +++ b/backend/open_webui/routers/ollama.py @@ -418,21 +418,24 @@ async def get_all_models(request: Request, user: UserModel = None): async def get_filtered_models(models, user, db=None): # Filter models based on user access control model_ids = [model["model"] for model in models.get("models", [])] - model_infos = {m.id: m for m in Models.get_models_by_ids(model_ids, db=db)} - user_group_ids = {g.id for g in Groups.get_groups_by_member_id(user.id, db=db)} + model_infos = {model_info.id: model_info for model_info in Models.get_models_by_ids(model_ids, db=db)} + user_group_ids = {group.id for group in Groups.get_groups_by_member_id(user.id, db=db)} + + # Batch-fetch accessible resource IDs in a single query instead of N has_access calls + accessible_model_ids = AccessGrants.get_accessible_resource_ids( + user_id=user.id, + resource_type="model", + resource_ids=list(model_infos.keys()), + permission="read", + user_group_ids=user_group_ids, + db=db, + ) filtered_models = [] for model in models.get("models", []): model_info = model_infos.get(model["model"]) if model_info: - if user.id == model_info.user_id or AccessGrants.has_access( - user_id=user.id, - resource_type="model", - resource_id=model_info.id, - permission="read", - user_group_ids=user_group_ids, - db=db, - ): + if user.id == model_info.user_id or model_info.id in accessible_model_ids: filtered_models.append(model) return filtered_models @@ -1329,6 +1332,9 @@ async def generate_chat_completion( # Check if user has access to the model if not bypass_filter and user.role == "user": + user_group_ids = { + group.id for group in Groups.get_groups_by_member_id(user.id) + } if not ( user.id == model_info.user_id or AccessGrants.has_access( @@ -1336,6 +1342,7 @@ async def generate_chat_completion( resource_type="model", resource_id=model_info.id, permission="read", + user_group_ids=user_group_ids, ) ): raise HTTPException( @@ -1436,6 +1443,9 @@ async def generate_openai_completion( # Check if user has access to the model if user.role == "user": + user_group_ids = { + group.id for group in Groups.get_groups_by_member_id(user.id) + } if not ( user.id == model_info.user_id or AccessGrants.has_access( @@ -1443,6 +1453,7 @@ async def generate_openai_completion( resource_type="model", resource_id=model_info.id, permission="read", + user_group_ids=user_group_ids, ) ): raise HTTPException( @@ -1520,6 +1531,9 @@ async def generate_openai_chat_completion( # Check if user has access to the model if user.role == "user": + user_group_ids = { + group.id for group in Groups.get_groups_by_member_id(user.id) + } if not ( user.id == model_info.user_id or AccessGrants.has_access( @@ -1527,6 +1541,7 @@ async def generate_openai_chat_completion( resource_type="model", resource_id=model_info.id, permission="read", + user_group_ids=user_group_ids, ) ): raise HTTPException( @@ -1618,21 +1633,24 @@ async def get_openai_models( if user.role == "user" and not BYPASS_MODEL_ACCESS_CONTROL: # Filter models based on user access control model_ids = [model["id"] for model in models] - model_infos = {m.id: m for m in Models.get_models_by_ids(model_ids, db=db)} - user_group_ids = {g.id for g in Groups.get_groups_by_member_id(user.id, db=db)} + model_infos = {model_info.id: model_info for model_info in Models.get_models_by_ids(model_ids, db=db)} + user_group_ids = {group.id for group in Groups.get_groups_by_member_id(user.id, db=db)} + + # Batch-fetch accessible resource IDs in a single query instead of N has_access calls + accessible_model_ids = AccessGrants.get_accessible_resource_ids( + user_id=user.id, + resource_type="model", + resource_ids=list(model_infos.keys()), + permission="read", + user_group_ids=user_group_ids, + db=db, + ) filtered_models = [] for model in models: model_info = model_infos.get(model["id"]) if model_info: - if user.id == model_info.user_id or AccessGrants.has_access( - user_id=user.id, - resource_type="model", - resource_id=model_info.id, - permission="read", - user_group_ids=user_group_ids, - db=db, - ): + if user.id == model_info.user_id or model_info.id in accessible_model_ids: filtered_models.append(model) models = filtered_models diff --git a/backend/open_webui/routers/openai.py b/backend/open_webui/routers/openai.py index de978d011a..f8688a9c93 100644 --- a/backend/open_webui/routers/openai.py +++ b/backend/open_webui/routers/openai.py @@ -455,21 +455,24 @@ async def get_all_models_responses(request: Request, user: UserModel) -> list: async def get_filtered_models(models, user, db=None): # Filter models based on user access control model_ids = [model["id"] for model in models.get("data", [])] - model_infos = {m.id: m for m in Models.get_models_by_ids(model_ids, db=db)} - user_group_ids = {g.id for g in Groups.get_groups_by_member_id(user.id, db=db)} + model_infos = {model_info.id: model_info for model_info in Models.get_models_by_ids(model_ids, db=db)} + user_group_ids = {group.id for group in Groups.get_groups_by_member_id(user.id, db=db)} + + # Batch-fetch accessible resource IDs in a single query instead of N has_access calls + accessible_model_ids = AccessGrants.get_accessible_resource_ids( + user_id=user.id, + resource_type="model", + resource_ids=list(model_infos.keys()), + permission="read", + user_group_ids=user_group_ids, + db=db, + ) filtered_models = [] for model in models.get("data", []): model_info = model_infos.get(model["id"]) if model_info: - if user.id == model_info.user_id or AccessGrants.has_access( - user_id=user.id, - resource_type="model", - resource_id=model_info.id, - permission="read", - user_group_ids=user_group_ids, - db=db, - ): + if user.id == model_info.user_id or model_info.id in accessible_model_ids: filtered_models.append(model) return filtered_models @@ -960,6 +963,9 @@ async def generate_chat_completion( # Check if user has access to the model if not bypass_filter and user.role == "user": + user_group_ids = { + group.id for group in Groups.get_groups_by_member_id(user.id) + } if not ( user.id == model_info.user_id or AccessGrants.has_access( @@ -967,6 +973,7 @@ async def generate_chat_completion( resource_type="model", resource_id=model_info.id, permission="read", + user_group_ids=user_group_ids, ) ): raise HTTPException( diff --git a/backend/open_webui/routers/prompts.py b/backend/open_webui/routers/prompts.py index 86d2648a88..0060ab2b18 100644 --- a/backend/open_webui/routers/prompts.py +++ b/backend/open_webui/routers/prompts.py @@ -114,6 +114,17 @@ async def get_prompt_list( user.id, filter=filter, skip=skip, limit=limit, db=db ) + # Batch-fetch writable prompt IDs in a single query instead of N has_access calls + prompt_ids = [prompt.id for prompt in result.items] + writable_prompt_ids = AccessGrants.get_accessible_resource_ids( + user_id=user.id, + resource_type="prompt", + resource_ids=prompt_ids, + permission="write", + user_group_ids=user_group_ids, + db=db, + ) + return PromptAccessListResponse( items=[ PromptAccessResponse( @@ -121,14 +132,7 @@ async def get_prompt_list( write_access=( (user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL) or user.id == prompt.user_id - or AccessGrants.has_access( - user_id=user.id, - resource_type="prompt", - resource_id=prompt.id, - permission="write", - user_group_ids=user_group_ids, - db=db, - ) + or prompt.id in writable_prompt_ids ), ) for prompt in result.items diff --git a/backend/open_webui/utils/models.py b/backend/open_webui/utils/models.py index ff3a6e0caf..8bef1591bb 100644 --- a/backend/open_webui/utils/models.py +++ b/backend/open_webui/utils/models.py @@ -377,10 +377,21 @@ def get_filtered_models(models, user, db=None): for model_info in Models.get_models_by_ids(model_ids) } - filtered_models = [] user_group_ids = { group.id for group in Groups.get_groups_by_member_id(user.id, db=db) } + + # Batch-fetch accessible resource IDs in a single query instead of N has_access calls + accessible_model_ids = AccessGrants.get_accessible_resource_ids( + user_id=user.id, + resource_type="model", + resource_ids=list(model_infos.keys()), + permission="read", + user_group_ids=user_group_ids, + db=db, + ) + + filtered_models = [] for model in models: if model.get("arena"): meta = model.get("info", {}).get("meta", {}) @@ -399,14 +410,7 @@ def get_filtered_models(models, user, db=None): if ( (user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL) or user.id == model_info.user_id - or AccessGrants.has_access( - user_id=user.id, - resource_type="model", - resource_id=model_info.id, - permission="read", - user_group_ids=user_group_ids, - db=db, - ) + or model_info.id in accessible_model_ids ): filtered_models.append(model) From b7549d2f6ca2843661ec79a5a1e55da9e7553368 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Fri, 13 Feb 2026 14:08:07 -0600 Subject: [PATCH 15/34] refac: defer profile --- backend/open_webui/models/users.py | 27 +++++++++++++------ backend/open_webui/routers/auths.py | 4 +-- backend/open_webui/socket/main.py | 8 +++++- .../test/apps/webui/routers/test_users.py | 4 +-- 4 files changed, 30 insertions(+), 13 deletions(-) diff --git a/backend/open_webui/models/users.py b/backend/open_webui/models/users.py index fffeb32d3e..cff45ccb35 100644 --- a/backend/open_webui/models/users.py +++ b/backend/open_webui/models/users.py @@ -1,7 +1,7 @@ import time from typing import Optional -from sqlalchemy.orm import Session +from sqlalchemy.orm import Session, defer from open_webui.internal.db import Base, JSONField, get_db, get_db_context @@ -15,7 +15,7 @@ from open_webui.utils.validate import validate_profile_image_url -from pydantic import BaseModel, ConfigDict, field_validator +from pydantic import BaseModel, ConfigDict, field_validator, model_validator from sqlalchemy import ( BigInteger, JSON, @@ -28,7 +28,7 @@ select, cast, ) -from sqlalchemy import or_, case +from sqlalchemy import or_, case, func from sqlalchemy.dialects.postgresql import JSONB import datetime @@ -86,7 +86,7 @@ class UserModel(BaseModel): name: str - profile_image_url: str + profile_image_url: Optional[str] = None profile_banner_image_url: Optional[str] = None bio: Optional[str] = None @@ -110,6 +110,12 @@ class UserModel(BaseModel): model_config = ConfigDict(from_attributes=True) + @model_validator(mode="after") + def set_profile_image_url(self): + if not self.profile_image_url: + self.profile_image_url = f"/api/v1/users/{self.id}/profile/image" + return self + class UserStatusModel(UserModel): is_active: bool = False @@ -315,8 +321,12 @@ def get_user_by_email( ) -> Optional[UserModel]: try: with get_db_context(db) as db: - user = db.query(User).filter_by(email=email).first() - return UserModel.model_validate(user) + user = ( + db.query(User) + .filter(func.lower(User.email) == email.lower()) + .first() + ) + return UserModel.model_validate(user) if user else None except Exception: return None @@ -350,7 +360,7 @@ def get_users( ) -> dict: with get_db_context(db) as db: # Join GroupMember so we can order by group_id when requested - query = db.query(User) + query = db.query(User).options(defer(User.profile_image_url)) if filter: query_key = filter.get("query") @@ -485,6 +495,7 @@ def get_users_by_group_id( with get_db_context(db) as db: users = ( db.query(User) + .options(defer(User.profile_image_url)) .join(GroupMember, User.id == GroupMember.user_id) .filter(GroupMember.group_id == group_id) .all() @@ -495,7 +506,7 @@ def get_users_by_user_ids( self, user_ids: list[str], db: Optional[Session] = None ) -> list[UserStatusModel]: with get_db_context(db) as db: - users = db.query(User).filter(User.id.in_(user_ids)).all() + users = db.query(User).options(defer(User.profile_image_url)).filter(User.id.in_(user_ids)).all() return [UserModel.model_validate(user) for user in users] def get_num_users(self, db: Optional[Session] = None) -> Optional[int]: diff --git a/backend/open_webui/routers/auths.py b/backend/open_webui/routers/auths.py index 7f86307e80..8ada514cc2 100644 --- a/backend/open_webui/routers/auths.py +++ b/backend/open_webui/routers/auths.py @@ -145,7 +145,7 @@ def create_session_response( "email": user.email, "name": user.name, "role": user.role, - "profile_image_url": user.profile_image_url, + "profile_image_url": f"/api/v1/users/{user.id}/profile/image", "permissions": user_permissions, } @@ -926,7 +926,7 @@ async def add_user( "email": user.email, "name": user.name, "role": user.role, - "profile_image_url": user.profile_image_url, + "profile_image_url": f"/api/v1/users/{user.id}/profile/image", } else: raise HTTPException(500, detail=ERROR_MESSAGES.CREATE_USER_ERROR) diff --git a/backend/open_webui/socket/main.py b/backend/open_webui/socket/main.py index 0d762ee5b2..b43c56b4e6 100644 --- a/backend/open_webui/socket/main.py +++ b/backend/open_webui/socket/main.py @@ -314,7 +314,13 @@ async def connect(sid, environ, auth): if user: SESSION_POOL[sid] = user.model_dump( - exclude=["date_of_birth", "bio", "gender"] + exclude=[ + "profile_image_url", + "profile_banner_image_url", + "date_of_birth", + "bio", + "gender", + ] ) await sio.enter_room(sid, f"user:{user.id}") diff --git a/backend/open_webui/test/apps/webui/routers/test_users.py b/backend/open_webui/test/apps/webui/routers/test_users.py index 1a58ab147a..3108729710 100644 --- a/backend/open_webui/test/apps/webui/routers/test_users.py +++ b/backend/open_webui/test/apps/webui/routers/test_users.py @@ -12,7 +12,7 @@ def _assert_user(data, id, **kwargs): comparison_data = { "name": f"user {id}", "email": f"user{id}@openwebui.com", - "profile_image_url": f"/user{id}.png", + "profile_image_url": f"/api/v1/users/{id}/profile/image", "role": "user", **kwargs, } @@ -150,7 +150,7 @@ def test_users(self): role="admin", name="user 2 updated", email="user2-updated@openwebui.com", - profile_image_url="/user2-updated.png", + profile_image_url=f"/api/v1/users/2/profile/image", ) # Delete user by id From d1d1efe212b16e0052359991d67fd813125077e8 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Fri, 13 Feb 2026 14:27:11 -0600 Subject: [PATCH 16/34] refac: scim --- backend/open_webui/env.py | 8 ++ ...3d4e5f6a7_add_scim_column_to_user_table.py | 26 +++++ backend/open_webui/models/users.py | 59 +++++++++++ backend/open_webui/routers/scim.py | 98 +++++++++++++++++-- 4 files changed, 183 insertions(+), 8 deletions(-) create mode 100644 backend/open_webui/migrations/versions/b2c3d4e5f6a7_add_scim_column_to_user_table.py diff --git a/backend/open_webui/env.py b/backend/open_webui/env.py index 4ec2e2d8e2..9bbf0aecdd 100644 --- a/backend/open_webui/env.py +++ b/backend/open_webui/env.py @@ -572,6 +572,14 @@ def parse_section(section): == "true" ) SCIM_TOKEN = os.environ.get("SCIM_TOKEN", "") +SCIM_AUTH_PROVIDER = os.environ.get("SCIM_AUTH_PROVIDER", "") + +if ENABLE_SCIM and not SCIM_AUTH_PROVIDER: + log.warning( + "SCIM is enabled but SCIM_AUTH_PROVIDER is not set. " + "Set SCIM_AUTH_PROVIDER to the OAuth provider name (e.g. 'microsoft', 'oidc') " + "to enable externalId storage." + ) #################################### # LICENSE_KEY diff --git a/backend/open_webui/migrations/versions/b2c3d4e5f6a7_add_scim_column_to_user_table.py b/backend/open_webui/migrations/versions/b2c3d4e5f6a7_add_scim_column_to_user_table.py new file mode 100644 index 0000000000..e8bf9a850f --- /dev/null +++ b/backend/open_webui/migrations/versions/b2c3d4e5f6a7_add_scim_column_to_user_table.py @@ -0,0 +1,26 @@ +"""add scim column to user table + +Revision ID: b2c3d4e5f6a7 +Revises: a1b2c3d4e5f6 +Create Date: 2026-02-13 14:19:00.000000 + +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision: str = "b2c3d4e5f6a7" +down_revision: Union[str, None] = "a1b2c3d4e5f6" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.add_column("user", sa.Column("scim", sa.JSON(), nullable=True)) + + +def downgrade() -> None: + op.drop_column("user", "scim") diff --git a/backend/open_webui/models/users.py b/backend/open_webui/models/users.py index cff45ccb35..2eb76131ab 100644 --- a/backend/open_webui/models/users.py +++ b/backend/open_webui/models/users.py @@ -71,6 +71,7 @@ class User(Base): settings = Column(JSON, nullable=True) oauth = Column(JSON, nullable=True) + scim = Column(JSON, nullable=True) last_active_at = Column(BigInteger) updated_at = Column(BigInteger) @@ -103,6 +104,7 @@ class UserModel(BaseModel): settings: Optional[UserSettings] = None oauth: Optional[dict] = None + scim: Optional[dict] = None last_active_at: int # timestamp in epoch updated_at: int # timestamp in epoch @@ -351,6 +353,31 @@ def get_user_by_oauth_sub( # You may want to log the exception here return None + def get_user_by_scim_external_id( + self, provider: str, external_id: str, db: Optional[Session] = None + ) -> Optional[UserModel]: + try: + with get_db_context(db) as db: # type: Session + dialect_name = db.bind.dialect.name + + query = db.query(User) + if dialect_name == "sqlite": + query = query.filter( + User.scim.contains( + {provider: {"external_id": external_id}} + ) + ) + elif dialect_name == "postgresql": + query = query.filter( + User.scim[provider].cast(JSONB)["external_id"].astext + == external_id + ) + + user = query.first() + return UserModel.model_validate(user) if user else None + except Exception: + return None + def get_users( self, filter: Optional[dict] = None, @@ -646,6 +673,38 @@ def update_user_oauth_by_id( except Exception: return None + def update_user_scim_by_id( + self, + id: str, + provider: str, + external_id: str, + db: Optional[Session] = None, + ) -> Optional[UserModel]: + """ + Update or insert a SCIM provider/external_id pair into the user's scim JSON field. + Example resulting structure: + { + "microsoft": { "external_id": "abc" }, + "okta": { "external_id": "def" } + } + """ + try: + with get_db_context(db) as db: + user = db.query(User).filter_by(id=id).first() + if not user: + return None + + scim = user.scim or {} + scim[provider] = {"external_id": external_id} + + db.query(User).filter_by(id=id).update({"scim": scim}) + db.commit() + + return UserModel.model_validate(user) + + except Exception: + return None + def update_user_by_id( self, id: str, updated: dict, db: Optional[Session] = None ) -> Optional[UserModel]: diff --git a/backend/open_webui/routers/scim.py b/backend/open_webui/routers/scim.py index 681be3c7d2..13d3b5bdf2 100644 --- a/backend/open_webui/routers/scim.py +++ b/backend/open_webui/routers/scim.py @@ -25,6 +25,9 @@ ) from open_webui.constants import ERROR_MESSAGES +from open_webui.config import OAUTH_PROVIDERS +from open_webui.env import SCIM_AUTH_PROVIDER + from sqlalchemy.orm import Session from open_webui.internal.db import get_session @@ -300,6 +303,45 @@ def get_scim_auth( ) +def get_external_id(user: UserModel) -> Optional[str]: + """Extract externalId from a user's scim data. + + Checks all stored provider entries and returns the first external_id found. + """ + if not user.scim: + return None + for provider_data in user.scim.values(): + if isinstance(provider_data, dict) and "external_id" in provider_data: + return provider_data["external_id"] + return None + + +def get_scim_provider() -> str: + """Return the configured SCIM auth provider. + + Requires SCIM_AUTH_PROVIDER env var to be set (e.g. 'microsoft', 'oidc'). + """ + if not SCIM_AUTH_PROVIDER: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="SCIM_AUTH_PROVIDER environment variable is required when SCIM is enabled", + ) + return SCIM_AUTH_PROVIDER + + +def find_user_by_external_id( + external_id: str, db=None +) -> Optional[UserModel]: + """Find a user by SCIM externalId, falling back to OAuth sub match.""" + provider = get_scim_provider() + user = Users.get_user_by_scim_external_id(provider, external_id, db=db) + if user: + return user + + # Fallback: check if externalId matches an existing OAuth sub (account linking) + return Users.get_user_by_oauth_sub(provider, external_id, db=db) + + def user_to_scim(user: UserModel, request: Request, db=None) -> SCIMUser: """Convert internal User model to SCIM User""" # Parse display name into name components @@ -321,6 +363,7 @@ def user_to_scim(user: UserModel, request: Request, db=None) -> SCIMUser: return SCIMUser( id=user.id, + externalId=get_external_id(user), userName=user.email, name=SCIMName( formatted=user.name, @@ -494,13 +537,17 @@ async def get_users( # Get users from database if filter: - # Simple filter parsing - supports userName eq "email" - # In production, you'd want a more robust filter parser + # Simple filter parsing - supports userName eq, externalId eq if "userName eq" in filter: email = filter.split('"')[1] user = Users.get_user_by_email(email, db=db) users_list = [user] if user else [] total = 1 if user else 0 + elif "externalId eq" in filter: + external_id = filter.split('"')[1] + user = find_user_by_external_id(external_id, db=db) + users_list = [user] if user else [] + total = 1 if user else 0 else: response = Users.get_users(skip=skip, limit=limit, db=db) users_list = response["users"] @@ -546,17 +593,33 @@ async def create_user( db: Session = Depends(get_session), ): """Create SCIM User""" - # Check if user already exists - existing_user = Users.get_user_by_email(user_data.userName, db=db) + # Check for duplicate by externalId + if user_data.externalId: + existing_user = find_user_by_external_id(user_data.externalId, db=db) + if existing_user: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail=f"User with externalId {user_data.externalId} already exists", + ) + + # Determine primary email (lowercased per RFC 5321) + email = user_data.userName + for entry in user_data.emails: + if entry.primary: + email = entry.value + break + email = email.lower() + + # Check for duplicate by email + existing_user = Users.get_user_by_email(email, db=db) if existing_user: raise HTTPException( status_code=status.HTTP_409_CONFLICT, - detail=f"User with email {user_data.userName} already exists", + detail=f"User with email {email} already exists", ) # Create user user_id = str(uuid.uuid4()) - email = user_data.emails[0].value if user_data.emails else user_data.userName # Parse name if provided name = user_data.displayName @@ -571,7 +634,6 @@ async def create_user( if user_data.photos and len(user_data.photos) > 0: profile_image = user_data.photos[0].value - # Create user new_user = Users.insert_new_user( id=user_id, name=name, @@ -587,6 +649,14 @@ async def create_user( detail="Failed to create user", ) + # Store externalId in the scim field + if user_data.externalId: + provider = get_scim_provider() + Users.update_user_scim_by_id( + user_id, provider, user_data.externalId, db=db + ) + new_user = Users.get_user_by_id(user_id, db=db) + return user_to_scim(new_user, request, db=db) @@ -631,7 +701,6 @@ async def update_user( if user_data.photos and len(user_data.photos) > 0: update_data["profile_image_url"] = user_data.photos[0].value - # Update user updated_user = Users.update_user_by_id(user_id, update_data, db=db) if not updated_user: raise HTTPException( @@ -639,6 +708,14 @@ async def update_user( detail="Failed to update user", ) + # Update externalId in the scim field + if user_data.externalId: + provider = get_scim_provider() + Users.update_user_scim_by_id( + user_id, provider, user_data.externalId, db=db + ) + updated_user = Users.get_user_by_id(user_id, db=db) + return user_to_scim(updated_user, request, db=db) @@ -676,6 +753,11 @@ async def patch_user( update_data["email"] = value elif path == "name.formatted": update_data["name"] = value + elif path == "externalId": + provider = get_scim_provider() + Users.update_user_scim_by_id( + user_id, provider, value, db=db + ) # Update user if update_data: From 370a677a38bad1400952551f8b169b499566b47d Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Fri, 13 Feb 2026 21:28:23 +0100 Subject: [PATCH 17/34] fix: pin torch to prevent startup errors on ARM devices (#21385) * fix: rpi * Update requirements-min.txt * Update requirements.txt * Update pyproject.toml --- Dockerfile | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 1608a24afe..1c0ae0b3c8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -137,7 +137,8 @@ COPY --chown=$UID:$GID ./backend/requirements.txt ./requirements.txt RUN pip3 install --no-cache-dir uv && \ if [ "$USE_CUDA" = "true" ]; then \ # If you use CUDA the whisper and embedding model will be downloaded on first use - pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/$USE_CUDA_DOCKER_VER --no-cache-dir && \ + # fix: pin torch<=2.9.1 - torch 2.10.0 aarch64 wheels cause SIGILL on ARM devices (RPi 4 Cortex-A72) #21349 + pip3 install 'torch<=2.9.1' torchvision torchaudio --index-url https://download.pytorch.org/whl/$USE_CUDA_DOCKER_VER --no-cache-dir && \ uv pip install --system -r requirements.txt --no-cache-dir && \ python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ['RAG_EMBEDDING_MODEL'], device='cpu')" && \ python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ.get('AUXILIARY_EMBEDDING_MODEL', 'TaylorAI/bge-micro-v2'), device='cpu')" && \ @@ -145,7 +146,7 @@ RUN pip3 install --no-cache-dir uv && \ python -c "import os; import tiktoken; tiktoken.get_encoding(os.environ['TIKTOKEN_ENCODING_NAME'])"; \ python -c "import nltk; nltk.download('punkt_tab')"; \ else \ - pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu --no-cache-dir && \ + pip3 install 'torch<=2.9.1' torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu --no-cache-dir && \ uv pip install --system -r requirements.txt --no-cache-dir && \ if [ "$USE_SLIM" != "true" ]; then \ python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ['RAG_EMBEDDING_MODEL'], device='cpu')" && \ From f027a01ab2ff3b6175af3dd13a4478c265c0544a Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Fri, 13 Feb 2026 14:39:01 -0600 Subject: [PATCH 18/34] fix: direct model access control --- backend/open_webui/routers/models.py | 27 ++++++++++++++++--- .../workspace/Models/ModelEditor.svelte | 2 +- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/backend/open_webui/routers/models.py b/backend/open_webui/routers/models.py index fe4137cab9..aa573dd720 100644 --- a/backend/open_webui/routers/models.py +++ b/backend/open_webui/routers/models.py @@ -8,7 +8,9 @@ from open_webui.models.groups import Groups from open_webui.models.models import ( ModelForm, + ModelMeta, ModelModel, + ModelParams, ModelResponse, ModelListResponse, ModelAccessListResponse, @@ -521,11 +523,30 @@ async def update_model_access_by_id( db: Session = Depends(get_session), ): model = Models.get_model_by_id(form_data.id, db=db) + + # Non-preset models (e.g. direct Ollama/OpenAI models) may not have a DB + # entry yet. Create a minimal one so access grants can be stored. if not model: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=ERROR_MESSAGES.NOT_FOUND, + if user.role != "admin": + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + model = Models.insert_new_model( + ModelForm( + id=form_data.id, + name=form_data.id, + meta=ModelMeta(), + params=ModelParams(), + ), + user.id, + db=db, ) + if not model: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=ERROR_MESSAGES.DEFAULT("Error creating model entry"), + ) if ( model.user_id != user.id diff --git a/src/lib/components/workspace/Models/ModelEditor.svelte b/src/lib/components/workspace/Models/ModelEditor.svelte index 80abd65445..292be8ceeb 100644 --- a/src/lib/components/workspace/Models/ModelEditor.svelte +++ b/src/lib/components/workspace/Models/ModelEditor.svelte @@ -349,7 +349,7 @@ await updateModelAccessGrants(localStorage.token, model.id, accessGrants); toast.success($i18n.t('Saved')); } catch (error) { - toast.error(`${error}`); + toast.error(error?.detail ?? `${error}`); } } }} From 163211a3676c1612fb88c34af149193eea5de95c Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Fri, 13 Feb 2026 14:43:06 -0600 Subject: [PATCH 19/34] refac: styling --- .../components/workspace/Models/ModelEditor.svelte | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/lib/components/workspace/Models/ModelEditor.svelte b/src/lib/components/workspace/Models/ModelEditor.svelte index 292be8ceeb..a00e2216b3 100644 --- a/src/lib/components/workspace/Models/ModelEditor.svelte +++ b/src/lib/components/workspace/Models/ModelEditor.svelte @@ -466,11 +466,11 @@ }} >
-
-
+
+