feat(plugins): release Copilot SDK Pipe v0.8.0 and Files Filter v0.1.3 (#50)

* feat(plugins): release copilot sdk pipe v0.8.0 and files filter v0.1.3

- Add P1~P4 conditional tool filtering and admin/server gating behavior

- Fix artifact publishing reliability, strict /api file URLs, and HTML preview/download delivery

- Update bilingual README/docs, release notes, and filter matching/debug improvements

* fix(docs): remove duplicate code block in tool-filtering zh doc

- Remove incorrectly placed duplicate 'if not is_enabled: continue' block
  outside code fence on line 161-163 of copilot-sdk-tool-filtering.zh.md
- Addresses review comment from gemini-code-assist (#50)
This commit is contained in:
Fu-Jie
2026-02-26 01:05:31 +08:00
committed by GitHub
parent 5b6dddd517
commit db33f44cbc
20 changed files with 1175 additions and 247 deletions

View File

@@ -1,11 +1,16 @@
# GitHub Copilot SDK Files Filter
**Author:** [Fu-Jie](https://github.com/Fu-Jie/openwebui-extensions) | **Version:** 0.1.2 | **Project:** [OpenWebUI Extensions](https://github.com/Fu-Jie/openwebui-extensions) | **License:** MIT
**Author:** [Fu-Jie](https://github.com/Fu-Jie/openwebui-extensions) | **Version:** 0.1.3 | **Project:** [OpenWebUI Extensions](https://github.com/Fu-Jie/openwebui-extensions) | **License:** MIT
This is a dedicated **companion filter plugin** designed specifically for the [GitHub Copilot SDK Pipe](https://openwebui.com/posts/github_copilot_official_sdk_pipe_ce96f7b4).
Its core mission is to **protect user-uploaded files from being "pre-processed" by the OpenWebUI core system, ensuring that the Copilot Agent receives the raw files for autonomous analysis.**
## ✨ v0.1.3 Updates (What's New)
- **🔍 BYOK Model ID Matching Fixed**: Now correctly identifies models in `github_copilot_official_sdk_pipe.xxx` format via prefix matching, in addition to keyword fallback for backward compatibility. (v0.1.3)
- **🐛 Dual-channel Debug Log**: Added `show_debug_log` valve. When enabled, logs are written to both server-side logger and browser console (`console.group`). (v0.1.3)
## 🎯 Why is this needed?
In OpenWebUI's default workflow, when you upload a file (e.g., PDF, Excel, Python script), OpenWebUI automatically initiates a **RAG (Retrieval-Augmented Generation)** process: parsing the file, vectorizing it, extracting text, and injecting it into the prompt.
@@ -49,5 +54,5 @@ Default settings work for most users:
## ⚠️ Important Notes
* **Must be used with Copilot SDK Pipe**: If you install this plugin without the main Pipe plugin, uploaded files will simply "disappear" (as no subsequent plugin will look for them).
* **Gemini Filter Compatibility**: Fully compatible with the Gemini Multimodal Filter. Just ensure priorities don't conflict.
- **Must be used with Copilot SDK Pipe**: If you install this plugin without the main Pipe plugin, uploaded files will simply "disappear" (as no subsequent plugin will look for them).
- **Gemini Filter Compatibility**: Fully compatible with the Gemini Multimodal Filter. Just ensure priorities don't conflict.

View File

@@ -1,11 +1,16 @@
# GitHub Copilot SDK 文件过滤器
**作者:** [Fu-Jie](https://github.com/Fu-Jie/openwebui-extensions) | **版本:** 0.1.2 | **项目:** [OpenWebUI Extensions](https://github.com/Fu-Jie/openwebui-extensions) | **许可证:** MIT
**作者:** [Fu-Jie](https://github.com/Fu-Jie/openwebui-extensions) | **版本:** 0.1.3 | **项目:** [OpenWebUI Extensions](https://github.com/Fu-Jie/openwebui-extensions) | **许可证:** MIT
这是一个专门为 [GitHub Copilot SDK Pipe](https://openwebui.com/posts/github_copilot_official_sdk_pipe_ce96f7b4) 设计的**伴侣过滤器插件**。
它的核心使命是:**保护用户上传的文件不被 OpenWebUI 核心系统“抢先处理”,确保 Copilot Agent 能够接收到原始文件并进行自主分析。**
## ✨ 0.1.3 更新内容 (What's New)
- **🔍 BYOK 模型 ID 匹配修复**: 新增前缀匹配(`github_copilot_official_sdk_pipe.xxx` 格式),修复 BYOK 模型无法被正确识别的问题,关键词兜底保持向后兼容。(v0.1.3)
- **🐛 双通道调试日志**: 新增 `show_debug_log` 配置项,启用后同时向后端日志和浏览器控制台(`console.group`)输出调试信息。(v0.1.3)
## 🎯 为什么需要它?
在 OpenWebUI 的默认流程中,当你上传一个文件(如 PDF、Excel、Python 脚本OpenWebUI 会自动启动 **RAG检索增强生成** 流程:解析文件、向量化、提取文本并注入到提示词中。
@@ -47,5 +52,5 @@
## ⚠️ 注意事项
* **必须配合 Copilot SDK Pipe 使用**:如果你没有安装主 Pipe 插件,本插件将导致上传的文件“凭空消失”。
* **Gemini Filter 兼容性**:已完美兼容 Gemini 多模态过滤器。只要优先级设置正确,它们可以共存互不干扰。
- **必须配合 Copilot SDK Pipe 使用**:如果你没有安装主 Pipe 插件,本插件将导致上传的文件“凭空消失”。
- **Gemini Filter 兼容性**:已完美兼容 Gemini 多模态过滤器。只要优先级设置正确,它们可以共存互不干扰。

View File

@@ -4,13 +4,18 @@ id: github_copilot_sdk_files_filter
author: Fu-Jie
author_url: https://github.com/Fu-Jie/openwebui-extensions
funding_url: https://github.com/open-webui
version: 0.1.2
version: 0.1.3
openwebui_id: 403a62ee-a596-45e7-be65-fab9cc249dd6
description: A specialized filter to bypass OpenWebUI's default RAG for GitHub Copilot SDK models. It moves uploaded files to a safe location ('copilot_files') so the Copilot Pipe can process them natively without interference.
"""
from pydantic import BaseModel, Field
from typing import Optional, Callable, Awaitable
import logging
import json
logger = logging.getLogger(__name__)
class Filter:
@@ -23,10 +28,62 @@ class Filter:
default="copilot_sdk",
description="Keyword to identify Copilot models (e.g., 'copilot_sdk').",
)
target_model_prefixes: str = Field(
default="github_copilot_official_sdk_pipe.,github_copilot_sdk_pipe.",
description="Comma-separated model id prefixes to identify Copilot SDK models.",
)
show_debug_log: bool = Field(
default=False,
description="Whether to print model matching debug logs in backend console.",
)
def __init__(self):
self.valves = self.Valves()
def _is_copilot_model(self, model_id: str) -> bool:
if not isinstance(model_id, str) or not model_id:
return False
current = model_id.strip().lower()
if not current:
return False
# 1) Prefix match (most reliable for OpenWebUI model id formats)
raw_prefixes = self.valves.target_model_prefixes or ""
prefixes = [p.strip().lower() for p in raw_prefixes.split(",") if p.strip()]
if any(current.startswith(prefix) for prefix in prefixes):
return True
# 2) Keyword fallback for backward compatibility
keyword = (self.valves.target_model_keyword or "").strip().lower()
return bool(keyword and keyword in current)
async def _emit_debug_log(
self,
__event_emitter__: Optional[Callable[[dict], Awaitable[None]]],
title: str,
data: dict,
):
if not self.valves.show_debug_log:
return
logger.info("[Copilot Files Filter] %s: %s", title, data)
if not __event_emitter__:
return
try:
js_code = f"""
(async function() {{
console.group('🧩 Copilot Files Filter: {title}');
console.log({json.dumps(data, ensure_ascii=False)});
console.groupEnd();
}})();
"""
await __event_emitter__({"type": "execute", "data": {"code": js_code}})
except Exception as e:
logger.debug("[Copilot Files Filter] frontend debug emit failed: %s", e)
async def inlet(
self,
body: dict,
@@ -45,8 +102,30 @@ class Filter:
current_model = base_model_id if base_model_id else body.get("model", "")
await self._emit_debug_log(
__event_emitter__,
"model-debug",
{
"body_model": body.get("model", ""),
"base_model_id": base_model_id,
"current_model": current_model,
},
)
# Check if it's a Copilot model
if self.valves.target_model_keyword.lower() in current_model.lower():
is_copilot_model = self._is_copilot_model(current_model)
await self._emit_debug_log(
__event_emitter__,
"match-result",
{
"is_copilot_model": is_copilot_model,
"prefixes": self.valves.target_model_prefixes,
"keyword": self.valves.target_model_keyword,
},
)
if is_copilot_model:
# If files exist, move them to 'copilot_files' and clear 'files'
# This prevents OpenWebUI from triggering RAG on these files
if "files" in body and body["files"]:

View File

@@ -1,6 +1,6 @@
# GitHub Copilot SDK Pipe for OpenWebUI
**Author:** [Fu-Jie](https://github.com/Fu-Jie) | **Version:** 0.7.0 | **Project:** [OpenWebUI Extensions](https://github.com/Fu-Jie/openwebui-extensions) | **License:** MIT
**Author:** [Fu-Jie](https://github.com/Fu-Jie) | **Version:** 0.8.0 | **Project:** [OpenWebUI Extensions](https://github.com/Fu-Jie/openwebui-extensions) | **License:** MIT
This is an advanced Pipe function for [OpenWebUI](https://github.com/open-webui/open-webui) that integrates the official [GitHub Copilot SDK](https://github.com/github/copilot-sdk). It enables you to use **GitHub Copilot models** (e.g., `gpt-5.2-codex`, `claude-sonnet-4.5`,`gemini-3-pro`, `gpt-5-mini`) **AND** your own models via **BYOK** (OpenAI, Anthropic) directly within OpenWebUI, providing a unified agentic experience with **strict User & Chat-level Workspace Isolation**.
@@ -14,13 +14,23 @@ This is an advanced Pipe function for [OpenWebUI](https://github.com/open-webui/
---
## ✨ v0.7.0 Updates (What's New)
## ✨ v0.8.0 Updates (What's New)
- **🚀 Integrated CLI Management**: The Copilot CLI is now automatically managed and bundled via the `github-copilot-sdk` pip package. No more manual `curl | bash` installation or version mismatches. (v0.7.0)
- **🧠 Native Tool Call UI**: Full adaptation to **OpenWebUI's native tool call UI** and thinking process visualization. (v0.7.0)
- **🏠 OpenWebUI v0.8.0+ Fix**: Resolved "Error getting file content" download failure by switching to absolute path registration for published files. (v0.7.0)
- **🌐 Comprehensive Multi-language Support**: Native localization for status messages in 11 languages (EN, ZH, JA, KO, FR, DE, ES, IT, RU, VI, ID). (v0.7.0)
- **🧹 Architecture Cleanup**: Refactored core setup and optimized reasoning status display for a leaner experience. (v0.7.0)
- **🎛️ Conditional Tool Filtering (P1~P4)**: Four-priority tool permission system. **Default ON**: If no tools are selected in Chat UI (P4), all enabled tools are active. **Whitelist Mode**: Once specific tools are checked, the whitelist strictly filters both OpenWebUI tools and MCP servers. Admin-level `config.enable` (P2) allows global server disabling. (v0.8.0)
- **🔧 File Publish Reliability**: Fixed `Error getting file content` across all storage backends (local/S3/GCS/Azure) by using `Storage.upload_file()` directly in the fallback path. HTML files are no longer blocked by `ALLOWED_FILE_EXTENSIONS` (`?process=false` always applied). (v0.8.0)
- **🌐 HTML Direct Access Link**: When `publish_file_from_workspace` publishes an HTML file, the plugin also provides a directly accessible HTML link for instant in-chat preview/opening. (v0.8.0)
- **🔒 Strict File URL Format**: Published file links must be relative paths starting with `/api/v1/files/` (e.g., `/api/v1/files/{id}/content/html`). Do not use `api/...` and do not prepend any domain. (v0.8.0)
- **🛠️ CLI Built-in Tools Always Available**: `available_tools` is now always `None`, ensuring Copilot CLI built-ins (e.g. `bash`, `create_file`) are never silently blocked regardless of MCP configuration. (v0.8.0)
- **📌 Publish Tool Always Injected**: `publish_file_from_workspace` is no longer lost when `ENABLE_OPENWEBUI_TOOLS` is disabled. (v0.8.0)
- **⚠️ Code Interpreter Limitation**: The `code_interpreter` tool runs in a remote, ephemeral environment. A system prompt warning now clarifies that it cannot access local files or persist changes. (v0.8.0)
### 🐞 Bug Fixes in v0.8.0
- Fixed `{"detail":"[ERROR: Error getting file content]"}` when publishing files under object storage backends by replacing fallback manual copy/DB writes with `Storage.upload_file()`.
- Fixed HTML artifact upload being rejected by `ALLOWED_FILE_EXTENSIONS` by always appending `?process=false` on file upload API calls.
- Fixed invalid artifact links generated as `api/...` or domain-prefixed absolute URLs; links are now constrained to `/api/v1/files/...` relative paths.
- Fixed Copilot CLI built-ins being silently unavailable when no server tools were configured/loaded (which resulted in `available_tools=[]`); now `available_tools` remains `None`.
- Fixed `publish_file_from_workspace` disappearing when `ENABLE_OPENWEBUI_TOOLS` was disabled.
---
@@ -33,11 +43,23 @@ This is an advanced Pipe function for [OpenWebUI](https://github.com/open-webui/
- **🧠 Deep Database Integration**: Real-time persistence of TOD·O lists for long-running workflows.
- **🌊 Advanced Streaming**: Full support for thinking process/Chain of Thought visualization.
- **🖼️ Intelligent Multimodal**: Vision capabilities and raw file analysis support (bypasses RAG for direct binary access).
- **📤 Workspace Artifacts (`publish_file_from_workspace`)**: Agents can generate files (Excel, CSV, HTML reports, etc.) and provide **persistent download links** directly in the chat.
- **📤 Workspace Artifacts (`publish_file_from_workspace`)**: Agents can generate files (Excel, CSV, HTML reports, etc.) and provide **persistent download links** directly in the chat. For HTML files, a direct-access HTML link is also provided.
- **🖼️ Interactive Artifacts**: Automatically renders HTML/JS apps generated by the agent directly in the chat interface.
---
## 🧩 Companion Files Filter (Required for raw files)
`GitHub Copilot SDK Files Filter` is the companion plugin that prevents OpenWebUI's default RAG pre-processing from consuming uploaded files before the Pipe receives them.
- **What it does**: Moves uploaded files to `copilot_files` so the Pipe can access raw binaries directly.
- **Why it matters**: Without it, uploaded files may be parsed/vectorized early and the Agent may lose direct raw-file access.
- **v0.1.3 highlights**:
- BYOK model-id matching fix (supports `github_copilot_official_sdk_pipe.xxx` prefixes).
- Optional dual-channel debug log (`show_debug_log`) to backend logger + browser console.
---
## ⚙️ Core Configuration (Valves)
### 1. Administrator Settings (Base)
@@ -105,6 +127,15 @@ If this plugin has been useful, a **Star** on [OpenWebUI Extensions](https://git
2. Create **Fine-grained token**, granting **Account permissions** -> **Copilot Requests** access.
3. Paste the generated Token into the `GH_TOKEN` field in Valves.
### 3) Authentication Requirement (Mandatory)
You MUST configure **at least one** credential source:
- `GH_TOKEN` (GitHub Copilot subscription route), or
- `BYOK_API_KEY` (OpenAI/Anthropic route).
If neither is configured, the model list will not appear.
---
## 📋 Troubleshooting & Dependencies

View File

@@ -1,6 +1,6 @@
# GitHub Copilot SDK 官方管道
**作者:** [Fu-Jie](https://github.com/Fu-Jie/openwebui-extensions) | **版本:** 0.7.0 | **项目:** [OpenWebUI Extensions](https://github.com/Fu-Jie/openwebui-extensions) | **许可证:** MIT
**作者:** [Fu-Jie](https://github.com/Fu-Jie/openwebui-extensions) | **版本:** 0.8.0 | **项目:** [OpenWebUI Extensions](https://github.com/Fu-Jie/openwebui-extensions) | **许可证:** MIT
这是一个用于 [OpenWebUI](https://github.com/open-webui/open-webui) 的高级 Pipe 函数,深度集成了 **GitHub Copilot SDK**。它不仅支持 **GitHub Copilot 官方模型**(如 `gpt-5.2-codex`, `claude-sonnet-4.5`, `gemini-3-pro`, `gpt-5-mini`),还支持 **BYOK (自带 Key)** 模式对接自定义服务商OpenAI, Anthropic并具备**严格的用户与会话级工作区隔离**能力,提供统一且安全的 Agent 交互体验。
@@ -14,13 +14,23 @@
---
## ✨ 0.7.0 更新内容 (What's New)
## ✨ 0.8.0 更新内容 (What's New)
- **🚀 CLI 免维护集成**: Copilot CLI 现在通过 `github-copilot-sdk` pip 包自动同步管理,彻底告别手动 `curl | bash` 安装及版本不匹配问题。(v0.7.0)
- **🧠 原生工具调用 UI**: 全面适配 **OpenWebUI 原生工具调用 UI** 与模型思考过程(思维链)展示。(v0.7.0)
- **🏠 OpenWebUI v0.8.0+ 兼容性修复**: 通过切换为绝对路径注册发布文件彻底解决了“Error getting file content”无法下载到本地的问题。(v0.7.0)
- **🌐 全面的多语言支持**: 针对状态消息进行了 11 国语言的原生本地化 (中/英/日/韩/法/德/西/意/俄/越/印尼)。(v0.7.0)
- **🧹 架构精简**: 重构了初始化逻辑并优化了推理状态显示,提供更轻量稳健的体验。(v0.7.0)
- **🎛️ 条件工具过滤 (P1~P4)**: 四优先级工具权限体系。**默认全开**: 若未在 Chat UI (P4) 勾选任何工具,则默认启用所有工具;**白名单模式**: 一旦勾选特定工具,即刻进入严格过滤模式,且 MCP server 同步受控;管理员亦可通过 `config.enable` (P2) 全局禁用工具服务器。(v0.8.0)
- **🔧 文件发布全面修复**: 通过在回退路径直接调用 `Storage.upload_file()`彻底修复了所有存储后端local/S3/GCS/Azure下的 `Error getting file content` 问题;同时上传时自动携带 `?process=false`HTML 文件不再被 `ALLOWED_FILE_EXTENSIONS` 拦截。(v0.8.0)
- **🌐 HTML 直达链接**: 当 `publish_file_from_workspace` 发布的是 HTML 文件时,插件会额外提供可直接访问的 HTML 链接,便于在聊天中即时预览/打开。(v0.8.0)
- **🔒 文件链接格式严格约束**: 发布链接必须是以 `/api/v1/files/` 开头的相对路径(例如 `/api/v1/files/{id}/content/html`)。禁止使用 `api/...`,也禁止拼接任何域名。(v0.8.0)
- **🛠️ CLI 内置工具始终可用**: `available_tools` 统一设为 `None`Copilot CLI 内置工具(如 `bash``create_file`)无论 MCP 配置如何都不会被静默屏蔽。(v0.8.0)
- **📌 发布工具始终注入**: 即使 `ENABLE_OPENWEBUI_TOOLS` 关闭,`publish_file_from_workspace` 工具也不再丢失。(v0.8.0)
- **⚠️ 代码解释器限制**: `code_interpreter` 工具运行在远程临时环境中。系统提示词现已包含警告,明确指出该工具无法访问本地文件或持久化更改。(v0.8.0)
### 🐞 v0.8.0 Bug 修复说明
- 修复了对象存储后端发布文件时出现的 `{"detail":"[ERROR: Error getting file content]"}`,回退路径从手动复制/写库改为 `Storage.upload_file()`
- 修复了 HTML 产物被 `ALLOWED_FILE_EXTENSIONS` 拦截的问题,上传接口统一追加 `?process=false`
- 修复了产物链接偶发被生成成 `api/...` 或带域名绝对 URL 的问题,现统一限制为 `/api/v1/files/...` 相对路径。
- 修复了在未配置/未加载任何 server 工具时(最终出现 `available_tools=[]`Copilot CLI 内置工具被静默禁用的问题,现统一保持 `available_tools=None`
- 修复了 `ENABLE_OPENWEBUI_TOOLS` 关闭时 `publish_file_from_workspace` 工具丢失的问题。
---
@@ -33,11 +43,23 @@
- **🧠 深度数据库集成**: 实时持久化 TOD·O 列表到 UI 进度条。
- **🌊 深度推理展示**: 完整支持模型思考过程 (Thinking Process) 的流式渲染。
- **🖼️ 智能多模态**: 完整支持图像识别与附件上传分析(绕过 RAG 直接访问原始二进制内容)。
- **📤 工作区产物工具 (`publish_file_from_workspace`)**: Agent 可生成文件Excel、CSV、HTML 报告等)并直接在聊天中提供**持久化下载链接**。
- **📤 工作区产物工具 (`publish_file_from_workspace`)**: Agent 可生成文件Excel、CSV、HTML 报告等)并直接在聊天中提供**持久化下载链接**。若为 HTML 文件,还会额外提供可直接访问的 HTML 链接。
- **🖼️ 交互式伪影 (Artifacts)**: 自动渲染 Agent 生成的 HTML/JS 应用程序,直接在聊天界面交互。
---
## 🧩 配套 Files Filter原始文件必备
`GitHub Copilot SDK Files Filter` 是本 Pipe 的配套插件,用于阻止 OpenWebUI 默认 RAG 在 Pipe 接手前抢先处理上传文件。
- **作用**: 将上传文件移动到 `copilot_files`,让 Pipe 能直接读取原始二进制。
- **必要性**: 若未安装,文件可能被提前解析/向量化Agent 难以拿到原始文件。
- **v0.1.3 重点**:
- 修复 BYOK 模型 ID 识别(支持 `github_copilot_official_sdk_pipe.xxx` 前缀匹配)。
- 新增双通道调试日志(`show_debug_log`):后端 logger + 浏览器控制台。
---
## ⚙️ 核心配置参数 (Valves)
### 1. 管理员配置 (基础设置)
@@ -105,7 +127,16 @@
2. 创建 **Fine-grained token**,授予 **Account permissions** -> **Copilot Requests** 访问权限。
3. 将生成的 Token 填入插件的 `GH_TOKEN` 配置项中。
### 3) 配套插件 (强烈推荐)
### 3) 认证配置要求(必填)
你必须至少配置以下一种凭据:
- `GH_TOKEN`GitHub Copilot 官方订阅路径),或
- `BYOK_API_KEY`OpenAI/Anthropic 自带 Key 路径)。
如果两者都未配置,模型列表将不会出现。
### 4) 配套插件 (强烈推荐)
为了获得最佳的文件处理体验,请安装 [GitHub Copilot SDK Files Filter](https://openwebui.com/posts/403a62ee-a596-45e7-be65-fab9cc249dd6)。

View File

@@ -5,7 +5,7 @@ author_url: https://github.com/Fu-Jie/openwebui-extensions
funding_url: https://github.com/open-webui
openwebui_id: ce96f7b4-12fc-4ac3-9a01-875713e69359
description: Integrate GitHub Copilot SDK. Supports dynamic models, multi-turn conversation, streaming, multimodal input, infinite sessions, and frontend debug logging.
version: 0.7.0
version: 0.8.0
requirements: github-copilot-sdk==0.1.25
"""
@@ -17,11 +17,9 @@ import tempfile
import asyncio
import logging
import shutil
import subprocess
import hashlib
import aiohttp
import contextlib
import traceback
from pathlib import Path
from typing import Optional, Union, AsyncGenerator, List, Any, Dict, Literal, Tuple
from types import SimpleNamespace
@@ -46,6 +44,7 @@ from open_webui.models.tools import Tools
from open_webui.models.users import Users
from open_webui.models.files import Files, FileForm
from open_webui.config import UPLOAD_DIR, DATA_DIR
from open_webui.storage.provider import Storage
import mimetypes
import uuid
import shutil
@@ -138,7 +137,8 @@ BASE_GUIDELINES = (
" - **Philosophy**: Visual Artifacts (HTML/Mermaid) and Downloadable Files are **COMPLEMENTARY**. Always aim to provide BOTH: instant visual insight in the chat AND a persistent file for the user to keep.\n"
" - **The Rule**: When the user needs to *possess* data (download/export), you MUST publish it. Creating a local file alone is useless because the user cannot access your container.\n"
" - **Implicit Requests**: If asked to 'export', 'get link', or 'save', automatically trigger this sequence.\n"
" - **Execution Sequence**: 1. **Write Local**: Create file in `.` (current directory). 2. **Publish**: Call `publish_file_from_workspace(filename='your_file.ext')`. 3. **Link**: Present the `download_url` as a Markdown link.\n"
" - **Execution Sequence**: 1. **Write Local**: Create file in `.` (current directory). 2. **Publish**: Call `publish_file_from_workspace(filename='your_file.ext')`. 3. **Link**: Present the result based on file type. **For HTML files**: the tool returns both `download_url` (raw file) and `view_url` (`/content/html` format). You MUST present `view_url` as a **[Preview]** link that opens directly in the browser, AND `download_url` as a **[Download]** link. **For image files** (.png, .jpg, .gif, .svg, etc.): embed directly using `![caption](download_url)` — NEVER use a text link for images. For all other files: present `download_url` as a single download link.\n"
" - **URL Format is STRICT**: File links MUST be relative paths starting with `/api/v1/files/` (for example: `/api/v1/files/{id}/content` or `/api/v1/files/{id}/content/html`). NEVER output `api/...` (missing leading slash). NEVER prepend any domain (such as `https://example.com/...`) even if a domain appears in conversation context.\n"
" - **Bypass RAG**: This protocol automatically handles S3 storage and bypasses RAG, ensuring 100% accurate data delivery.\n"
"6. **TODO Visibility**: Every time you call the `update_todo` tool, you **MUST** immediately follow up with a beautifully formatted **Markdown summary** of the current TODO list. Use task checkboxes (`- [ ]`), progress indicators, and clear headings so the user can see the status directly in the chat.\n"
"7. **Python Execution Standard**: For ANY task requiring Python logic (not just data analysis), you **MUST NOT** embed multi-line code directly in a shell command (e.g., using `python -c` or `<< 'EOF'`).\n"
@@ -197,10 +197,6 @@ class Pipe:
default=True,
description="Enable Direct MCP Client connection (Recommended).",
)
ENABLE_TOOL_CACHE: bool = Field(
default=True,
description="Cache OpenWebUI tools and MCP servers (performance optimization).",
)
REASONING_EFFORT: Literal["low", "medium", "high", "xhigh"] = Field(
default="medium",
description="Reasoning effort level (low, medium, high). Only affects standard Copilot models (not BYOK).",
@@ -251,6 +247,10 @@ class Pipe:
default="/app/backend/data/uploads",
description="Path to OpenWebUI uploads directory (for file processing).",
)
MODEL_CACHE_TTL: int = Field(
default=3600,
description="Model list cache TTL in seconds. Set to 0 to disable cache (always fetch). Default: 3600 (1 hour).",
)
BYOK_TYPE: Literal["openai", "anthropic"] = Field(
default="openai",
@@ -314,10 +314,6 @@ class Pipe:
default=True,
description="Enable dynamic MCP server loading (overrides global).",
)
ENABLE_TOOL_CACHE: bool = Field(
default=True,
description="Enable Tool/MCP configuration caching for this user.",
)
# BYOK User Overrides
BYOK_API_KEY: str = Field(
@@ -352,8 +348,7 @@ class Pipe:
_model_cache: List[dict] = [] # Model list cache
_standard_model_ids: set = set() # Track standard model IDs
_last_byok_config_hash: str = "" # Track BYOK config for cache invalidation
_tool_cache = None # Cache for converted OpenWebUI tools
_mcp_server_cache = None # Cache for MCP server config
_last_model_cache_time: float = 0 # Timestamp of last model cache refresh
_env_setup_done = False # Track if env setup has been completed
_last_update_check = 0 # Timestamp of last CLI update check
@@ -717,46 +712,32 @@ class Pipe:
uv = self._get_user_valves(__user__)
enable_tools = uv.ENABLE_OPENWEBUI_TOOLS
enable_openapi = uv.ENABLE_OPENAPI_SERVER
enable_cache = uv.ENABLE_TOOL_CACHE
# 2. If all tool types are disabled, return empty immediately
# 2. Publish tool is always injected, regardless of other settings
chat_ctx = self._get_chat_context(body, __metadata__)
chat_id = chat_ctx.get("chat_id")
file_tool = self._get_publish_file_tool(__user__, chat_id, __request__)
final_tools = [file_tool] if file_tool else []
# 3. If all OpenWebUI tool types are disabled, skip loading and return early
if not enable_tools and not enable_openapi:
return []
return final_tools
# 3. Check Cache
if enable_cache and self._tool_cache is not None:
await self._emit_debug_log(
" Using cached OpenWebUI tools.", __event_call__
)
# Create a shallow copy to append user-specific tools without polluting cache
tools = list(self._tool_cache)
# 4. Extract chat-level tool selection (P4: user selection from Chat UI)
chat_tool_ids = None
if __metadata__ and isinstance(__metadata__, dict):
chat_tool_ids = __metadata__.get("tool_ids") or None
# Inject File Publish Tool
chat_ctx = self._get_chat_context(body, __metadata__)
chat_id = chat_ctx.get("chat_id")
file_tool = self._get_publish_file_tool(__user__, chat_id, __request__)
if file_tool:
tools.append(file_tool)
return tools
# Load OpenWebUI tools dynamically
# 5. Load OpenWebUI tools dynamically (always fresh, no cache)
openwebui_tools = await self._load_openwebui_tools(
body=body,
__user__=__user__,
__event_call__=__event_call__,
enable_tools=enable_tools,
enable_openapi=enable_openapi,
chat_tool_ids=chat_tool_ids,
)
# Update Cache
if enable_cache:
self._tool_cache = openwebui_tools
await self._emit_debug_log(
"✅ OpenWebUI tools cached for subsequent requests.", __event_call__
)
# Log details only when cache is cold
if openwebui_tools:
tool_names = [t.name for t in openwebui_tools]
await self._emit_debug_log(
@@ -770,15 +751,7 @@ class Pipe:
__event_call__,
)
# Create a shallow copy to append user-specific tools without polluting cache
final_tools = list(openwebui_tools)
# Inject File Publish Tool
chat_ctx = self._get_chat_context(body, __metadata__)
chat_id = chat_ctx.get("chat_id")
file_tool = self._get_publish_file_tool(__user__, chat_id, __request__)
if file_tool:
final_tools.append(file_tool)
final_tools.extend(openwebui_tools)
return final_tools
@@ -892,7 +865,9 @@ class Pipe:
import aiohttp
base_url = str(__request__.base_url).rstrip("/")
upload_url = f"{base_url}/api/v1/files/"
# ?process=false skips RAG processing AND the
# ALLOWED_FILE_EXTENSIONS restriction (which blocks html/htm)
upload_url = f"{base_url}/api/v1/files/?process=false"
async with aiohttp.ClientSession() as session:
with open(target_path, "rb") as f:
@@ -925,14 +900,25 @@ class Pipe:
except Exception as e:
logger.error(f"API upload failed: {e}")
# 4. Fallback: Manual DB Insert (Local only)
# 4. Fallback: Use Storage.upload_file directly (S3/Local/GCS/Azure compatible)
if not api_success:
file_id = str(uuid.uuid4())
safe_filename = target_path.name
dest_path = Path(UPLOAD_DIR) / f"{file_id}_{safe_filename}"
await asyncio.to_thread(shutil.copy2, target_path, dest_path)
storage_filename = f"{file_id}_{safe_filename}"
db_path = str(dest_path)
def _upload_via_storage():
with open(target_path, "rb") as f:
_, stored_path = Storage.upload_file(
f,
storage_filename,
{
"OpenWebUI-User-Id": user_id,
"OpenWebUI-File-Id": file_id,
},
)
return stored_path
db_path = await asyncio.to_thread(_upload_via_storage)
file_form = FileForm(
id=file_id,
@@ -943,7 +929,7 @@ class Pipe:
"name": safe_filename,
"content_type": mimetypes.guess_type(safe_filename)[0]
or "text/plain",
"size": os.path.getsize(dest_path),
"size": os.path.getsize(target_path),
"source": "copilot_workspace_publish",
"skip_rag": True,
},
@@ -952,16 +938,16 @@ class Pipe:
# 5. Result
download_url = f"/api/v1/files/{file_id}/content"
view_url = download_url
is_html = safe_filename.lower().endswith(".html")
is_html = safe_filename.lower().endswith((".html", ".htm"))
# For HTML files, if user is admin, provide a direct view link (/content/html)
if is_html and is_admin:
# For HTML files, provide a direct view link (/content/html) for browser preview
view_url = None
if is_html:
view_url = f"{download_url}/html"
# Localized output
msg = self._get_translation(user_lang, "publish_success")
if is_html and is_admin:
if is_html:
hint = self._get_translation(
user_lang,
"publish_hint_html",
@@ -977,13 +963,16 @@ class Pipe:
download_url=download_url,
)
return {
result = {
"file_id": file_id,
"filename": safe_filename,
"download_url": download_url,
"message": msg,
"hint": hint,
}
if is_html and view_url:
result["view_url"] = view_url
return result
except Exception as e:
return {"error": str(e)}
@@ -1206,6 +1195,31 @@ class Pipe:
params_type=ParamsModel,
)(_tool)
def _read_tool_server_connections(self) -> list:
"""
Read tool server connections directly from the database to avoid stale
in-memory state in multi-worker deployments.
Falls back to the in-memory PersistentConfig value if DB read fails.
"""
try:
from open_webui.config import get_config
config_data = get_config()
connections = config_data.get("tool_server", {}).get("connections", None)
if connections is not None:
return connections if isinstance(connections, list) else []
except Exception as e:
logger.debug(
f"[Tools] DB config read failed, using in-memory fallback: {e}"
)
# Fallback: in-memory value (may be stale in multi-worker)
if hasattr(TOOL_SERVER_CONNECTIONS, "value") and isinstance(
TOOL_SERVER_CONNECTIONS.value, list
):
return TOOL_SERVER_CONNECTIONS.value
return []
def _build_openwebui_request(self, user: dict = None, token: str = None):
"""Build a more complete request-like object with dynamically loaded OpenWebUI configs."""
# Dynamically build config from the official registry
@@ -1219,12 +1233,9 @@ class Pipe:
setattr(config, item.env_name, val)
# Critical Fix: Explicitly sync TOOL_SERVER_CONNECTIONS to ensure OpenAPI tools work
# PERSISTENT_CONFIG_REGISTRY might not contain TOOL_SERVER_CONNECTIONS in all versions
if not hasattr(config, "TOOL_SERVER_CONNECTIONS"):
if hasattr(TOOL_SERVER_CONNECTIONS, "value"):
config.TOOL_SERVER_CONNECTIONS = TOOL_SERVER_CONNECTIONS.value
else:
config.TOOL_SERVER_CONNECTIONS = TOOL_SERVER_CONNECTIONS
# Read directly from DB to avoid stale in-memory state in multi-worker deployments
fresh_connections = self._read_tool_server_connections()
config.TOOL_SERVER_CONNECTIONS = fresh_connections
app_state = SimpleNamespace(
config=config,
@@ -1282,6 +1293,7 @@ class Pipe:
__event_call__=None,
enable_tools: bool = True,
enable_openapi: bool = True,
chat_tool_ids: Optional[list] = None,
):
"""Load OpenWebUI tools and convert them to Copilot SDK tools."""
if isinstance(__user__, (list, tuple)):
@@ -1300,17 +1312,20 @@ class Pipe:
# --- PROBE LOG ---
if __event_call__:
conn_status = "Missing"
if hasattr(TOOL_SERVER_CONNECTIONS, "value"):
val = TOOL_SERVER_CONNECTIONS.value
conn_status = (
f"List({len(val)})" if isinstance(val, list) else str(type(val))
)
conn_list = self._read_tool_server_connections()
conn_summary = []
for i, s in enumerate(conn_list):
if isinstance(s, dict):
s_id = s.get("info", {}).get("id") or s.get("id") or str(i)
s_type = s.get("type", "openapi")
s_enabled = s.get("config", {}).get("enable", False)
conn_summary.append(
{"id": s_id, "type": s_type, "enable": s_enabled}
)
await self._emit_debug_log(
f"[Tools Debug] Entry. UserID: {user_id}, EnableTools: {enable_tools}, EnableOpenAPI: {enable_openapi}, Connections: {conn_status}",
f"[Tools] TOOL_SERVER_CONNECTIONS ({len(conn_summary)} entries): {conn_summary}",
__event_call__,
debug_enabled=True,
)
# -----------------
@@ -1327,67 +1342,85 @@ class Pipe:
# 2. Get OpenAPI Tool Server tools
if enable_openapi:
if hasattr(TOOL_SERVER_CONNECTIONS, "value"):
raw_connections = TOOL_SERVER_CONNECTIONS.value
raw_connections = self._read_tool_server_connections()
# Handle Pydantic model vs List vs Dict
connections = []
if isinstance(raw_connections, list):
connections = raw_connections
elif hasattr(raw_connections, "dict"):
connections = raw_connections.dict()
elif hasattr(raw_connections, "model_dump"):
connections = raw_connections.model_dump()
# Handle Pydantic model vs List vs Dict
connections = []
if isinstance(raw_connections, list):
connections = raw_connections
elif hasattr(raw_connections, "dict"):
connections = raw_connections.dict()
elif hasattr(raw_connections, "model_dump"):
connections = raw_connections.model_dump()
# Debug logging for connections
if self.valves.DEBUG:
await self._emit_debug_log(
f"[Tools] Found {len(connections)} server connections (Type: {type(raw_connections)})",
__event_call__,
)
for idx, server in enumerate(connections):
# Handle server item type
s_type = (
server.get("type", "openapi")
if isinstance(server, dict)
else getattr(server, "type", "openapi")
)
# P2: config.enable check — skip admin-disabled servers
s_config = (
server.get("config", {})
if isinstance(server, dict)
else getattr(server, "config", {})
)
s_enabled = (
s_config.get("enable", False)
if isinstance(s_config, dict)
else getattr(s_config, "enable", False)
)
if not s_enabled:
if self.valves.DEBUG:
await self._emit_debug_log(
f"[Tools] Skipped disabled server at index {idx}",
__event_call__,
)
continue
# Handle server ID: Priority info.id > server.id > index
s_id = None
if isinstance(server, dict):
info = server.get("info", {})
s_id = info.get("id") or server.get("id")
else:
info = getattr(server, "info", {})
if isinstance(info, dict):
s_id = info.get("id")
else:
s_id = getattr(info, "id", None)
if not s_id:
s_id = getattr(server, "id", None)
if not s_id:
s_id = str(idx)
# Debug logging for connections
if self.valves.DEBUG:
await self._emit_debug_log(
f"[Tools] Found {len(connections)} server connections (Type: {type(raw_connections)})",
f"[Tools] Checking Server: ID={s_id}, Type={s_type}",
__event_call__,
)
for idx, server in enumerate(connections):
# Handle server item type
s_type = (
server.get("type", "openapi")
if isinstance(server, dict)
else getattr(server, "type", "openapi")
if s_type == "openapi":
# Ensure we don't add empty IDs, though fallback to idx should prevent this
if s_id:
tool_ids.append(f"server:{s_id}")
elif self.valves.DEBUG:
await self._emit_debug_log(
f"[Tools] Skipped non-OpenAPI server: {s_id} ({s_type})",
__event_call__,
)
# Handle server ID: Priority info.id > server.id > index
s_id = None
if isinstance(server, dict):
info = server.get("info", {})
s_id = info.get("id") or server.get("id")
else:
info = getattr(server, "info", {})
if isinstance(info, dict):
s_id = info.get("id")
else:
s_id = getattr(info, "id", None)
if not s_id:
s_id = getattr(server, "id", None)
if not s_id:
s_id = str(idx)
if self.valves.DEBUG:
await self._emit_debug_log(
f"[Tools] Checking Server: ID={s_id}, Type={s_type}",
__event_call__,
)
if s_type == "openapi":
# Ensure we don't add empty IDs, though fallback to idx should prevent this
if s_id:
tool_ids.append(f"server:{s_id}")
elif self.valves.DEBUG:
await self._emit_debug_log(
f"[Tools] Skipped non-OpenAPI server: {s_id} ({s_type})",
__event_call__,
)
if (
not tool_ids and not enable_tools
): # No IDs and no built-ins either if tools disabled
@@ -1397,6 +1430,16 @@ class Pipe:
)
return []
# P4: Chat tool_ids whitelist — only active when user explicitly selected tools
if chat_tool_ids:
chat_tool_ids_set = set(chat_tool_ids)
filtered = [tid for tid in tool_ids if tid in chat_tool_ids_set]
await self._emit_debug_log(
f"[Tools] tool_ids whitelist active: {len(tool_ids)}{len(filtered)} (selected: {chat_tool_ids})",
__event_call__,
)
tool_ids = filtered
if self.valves.DEBUG and tool_ids:
await self._emit_debug_log(
f"[Tools] Requesting tool IDs: {tool_ids}", __event_call__
@@ -1469,7 +1512,28 @@ class Pipe:
# Fetch Built-in Tools (Web Search, Memory, etc.)
if enable_tools:
try:
# Resolve real model dict from DB to respect meta.builtinTools config
model_dict = {}
model_id = body.get("model", "") if isinstance(body, dict) else ""
if model_id:
try:
from open_webui.models.models import Models as _Models
model_record = _Models.get_model_by_id(model_id)
if model_record:
model_dict = {"info": model_record.model_dump()}
except Exception:
pass
# Get builtin tools
# Open all feature gates so filtering is driven solely by
# model.meta.builtinTools (defaults to all-enabled when absent).
all_features = {
"memory": True,
"web_search": True,
"image_generation": True,
"code_interpreter": True,
}
builtin_tools = get_builtin_tools(
self._build_openwebui_request(user_data),
{
@@ -1477,16 +1541,8 @@ class Pipe:
"__chat_id__": extra_params.get("__chat_id__"),
"__message_id__": extra_params.get("__message_id__"),
},
model={
"info": {
"meta": {
"capabilities": {
"web_search": True,
"image_generation": True,
}
}
}
}, # Mock capabilities to allow all globally enabled tools
features=all_features,
model=model_dict, # model.meta.builtinTools controls which categories are active
)
if builtin_tools:
tools_dict.update(builtin_tools)
@@ -1504,16 +1560,15 @@ class Pipe:
tool_metadata_cache = {}
server_metadata_cache = {}
# Pre-build server metadata cache from TOOL_SERVER_CONNECTIONS
if hasattr(TOOL_SERVER_CONNECTIONS, "value"):
for server in TOOL_SERVER_CONNECTIONS.value:
server_id = server.get("id") or server.get("info", {}).get("id")
if server_id:
info = server.get("info", {})
server_metadata_cache[server_id] = {
"name": info.get("name") or server_id,
"description": info.get("description", ""),
}
# Pre-build server metadata cache from DB-fresh tool server connections
for server in self._read_tool_server_connections():
server_id = server.get("id") or server.get("info", {}).get("id")
if server_id:
info = server.get("info", {})
server_metadata_cache[server_id] = {
"name": info.get("name") or server_id,
"description": info.get("description", ""),
}
for tool_name, tool_def in tools_dict.items():
tool_id = tool_def.get("tool_id", "")
@@ -1578,7 +1633,10 @@ class Pipe:
return converted_tools
def _parse_mcp_servers(
self, __event_call__=None, enable_mcp: bool = True, enable_cache: bool = True
self,
__event_call__=None,
enable_mcp: bool = True,
chat_tool_ids: Optional[list] = None,
) -> Optional[dict]:
"""
Dynamically load MCP servers from OpenWebUI TOOL_SERVER_CONNECTIONS.
@@ -1587,17 +1645,22 @@ class Pipe:
if not enable_mcp:
return None
# Check Cache
if enable_cache and self._mcp_server_cache is not None:
return self._mcp_server_cache
mcp_servers = {}
# Iterate over OpenWebUI Tool Server Connections
if hasattr(TOOL_SERVER_CONNECTIONS, "value"):
connections = TOOL_SERVER_CONNECTIONS.value
else:
connections = []
# Read MCP servers directly from DB to avoid stale in-memory cache
connections = self._read_tool_server_connections()
if __event_call__:
mcp_summary = []
for s in connections if isinstance(connections, list) else []:
if isinstance(s, dict) and s.get("type") == "mcp":
s_id = s.get("info", {}).get("id") or s.get("id", "?")
s_enabled = s.get("config", {}).get("enable", False)
mcp_summary.append({"id": s_id, "enable": s_enabled})
self._emit_debug_log_sync(
f"[MCP] TOOL_SERVER_CONNECTIONS MCP entries ({len(mcp_summary)}): {mcp_summary}",
__event_call__,
)
for conn in connections:
if conn.get("type") == "mcp":
@@ -1605,6 +1668,18 @@ class Pipe:
# Use ID from info or generate one
raw_id = info.get("id", f"mcp-server-{len(mcp_servers)}")
# P2: config.enable check — skip admin-disabled servers
mcp_config = conn.get("config", {})
if not mcp_config.get("enable", False):
self._emit_debug_log_sync(
f"[MCP] Skipped disabled server: {raw_id}", __event_call__
)
continue
# P4: chat_tool_ids whitelist — if user selected tools, only include matching servers
if chat_tool_ids and f"server:{raw_id}" not in chat_tool_ids:
continue
# Sanitize server_id (using same logic as tools)
server_id = re.sub(r"[^a-zA-Z0-9_-]", "_", raw_id)
if not server_id or re.match(r"^[_.-]+$", server_id):
@@ -1636,7 +1711,6 @@ class Pipe:
headers.update(custom_headers)
# Get filtering configuration
mcp_config = conn.get("config", {})
function_filter = mcp_config.get("function_name_filter_list", "")
allowed_tools = ["*"]
@@ -1658,10 +1732,6 @@ class Pipe:
f"🔌 MCP Integrated: {server_id}", __event_call__
)
# Update Cache
if self.valves.ENABLE_TOOL_CACHE:
self._mcp_server_cache = mcp_servers
return mcp_servers if mcp_servers else None
async def _emit_debug_log(
@@ -1899,6 +1969,22 @@ class Pipe:
)
break
# Append Code Interpreter Warning
code_interpreter_warning = (
"\n\n[System Note]\n"
"The `execute_code` tool (builtin category: `code_interpreter`) executes code in a remote, ephemeral environment. "
"It cannot access files in your local workspace or persist changes. "
"Use it only for calculation or logic verification, not for file manipulation."
"\n"
"For links returned by `publish_file_from_workspace`, URL formatting is strict: "
"always use relative paths that start with `/api/v1/files/`. "
"Do not output `api/...` and do not prepend any domain."
)
if system_prompt_content:
system_prompt_content += code_interpreter_warning
else:
system_prompt_content = code_interpreter_warning.strip()
return system_prompt_content, system_prompt_source
def _get_workspace_dir(self, user_id: str = None, chat_id: str = None) -> str:
@@ -1969,7 +2055,7 @@ class Pipe:
is_admin: bool = False,
user_id: str = None,
enable_mcp: bool = True,
enable_cache: bool = True,
chat_tool_ids: Optional[list] = None,
__event_call__=None,
):
"""Build SessionConfig for Copilot SDK."""
@@ -2018,7 +2104,7 @@ class Pipe:
}
mcp_servers = self._parse_mcp_servers(
__event_call__, enable_mcp=enable_mcp, enable_cache=enable_cache
__event_call__, enable_mcp=enable_mcp, chat_tool_ids=chat_tool_ids
)
# Prepare session config parameters
@@ -2061,12 +2147,12 @@ class Pipe:
if mcp_servers:
session_params["mcp_servers"] = mcp_servers
# Critical Fix: When using MCP, available_tools must be None to allow dynamic discovery
session_params["available_tools"] = None
else:
session_params["available_tools"] = (
[t.name for t in custom_tools] if custom_tools else None
)
# Always set available_tools=None so the Copilot CLI's built-in tools
# (e.g. bash, create_file) remain accessible alongside our custom tools.
# Custom tools are registered via the 'tools' param; whitelist filtering
# via available_tools would silently block CLI built-ins.
session_params["available_tools"] = None
if provider_config:
session_params["provider"] = provider_config
@@ -2338,7 +2424,7 @@ class Pipe:
# 1. Environment Setup (Only if needed or not done)
if needs_setup:
self._setup_env(token=token, skip_cli_install=True)
self._setup_env(token=token)
self.__class__._last_update_check = now
else:
# Still inject token for BYOK real-time updates
@@ -2380,6 +2466,19 @@ class Pipe:
current_config_str = f"{token}|{uv.BYOK_BASE_URL or self.valves.BYOK_BASE_URL}|{uv.BYOK_API_KEY or self.valves.BYOK_API_KEY}|{self.valves.BYOK_BEARER_TOKEN}"
current_config_hash = hashlib.md5(current_config_str.encode()).hexdigest()
# TTL-based cache expiry
cache_ttl = self.valves.MODEL_CACHE_TTL
if (
self._model_cache
and cache_ttl > 0
and (now - self.__class__._last_model_cache_time) > cache_ttl
):
if self.valves.DEBUG:
logger.info(
f"[Pipes] Model cache expired (TTL={cache_ttl}s). Invalidating."
)
self.__class__._model_cache = []
if (
self._model_cache
and self.__class__._last_byok_config_hash != current_config_hash
@@ -2397,8 +2496,12 @@ class Pipe:
if self.valves.DEBUG:
logger.info("[Pipes] Refreshing model cache...")
try:
# Use effective token for fetching
self._setup_env(token=token, skip_cli_install=True)
# Use effective token for fetching.
# If COPILOT_CLI_PATH is missing (e.g. env cleared after worker restart),
# force a full re-discovery by resetting _env_setup_done first.
if not os.environ.get("COPILOT_CLI_PATH"):
self.__class__._env_setup_done = False
self._setup_env(token=token)
# Fetch BYOK models if configured
byok = []
@@ -2478,7 +2581,24 @@ class Pipe:
)
self._model_cache = standard + byok
self.__class__._last_model_cache_time = now
if not self._model_cache:
has_byok = bool(
(uv.BYOK_BASE_URL or self.valves.BYOK_BASE_URL)
and (
uv.BYOK_API_KEY
or self.valves.BYOK_API_KEY
or uv.BYOK_BEARER_TOKEN
or self.valves.BYOK_BEARER_TOKEN
)
)
if not token and not has_byok:
return [
{
"id": "no_token",
"name": "⚠️ No credentials configured. Please set GH_TOKEN or BYOK settings in Valves.",
}
]
return [
{
"id": "warming_up",
@@ -2530,8 +2650,6 @@ class Pipe:
debug_enabled: bool = False,
token: str = None,
enable_mcp: bool = True,
enable_cache: bool = True,
skip_cli_install: bool = False, # Kept for call-site compatibility, no longer used
__event_emitter__=None,
user_lang: str = "en-US",
):
@@ -2548,7 +2666,6 @@ class Pipe:
__event_call__,
debug_enabled,
enable_mcp=enable_mcp,
enable_cache=enable_cache,
)
return
@@ -2762,7 +2879,6 @@ class Pipe:
__event_call__=None,
debug_enabled: bool = False,
enable_mcp: bool = True,
enable_cache: bool = True,
):
"""Sync MCP configuration to ~/.copilot/config.json."""
path = os.path.expanduser("~/.copilot/config.json")
@@ -2786,9 +2902,7 @@ class Pipe:
pass
return
mcp = self._parse_mcp_servers(
__event_call__, enable_mcp=enable_mcp, enable_cache=enable_cache
)
mcp = self._parse_mcp_servers(__event_call__, enable_mcp=enable_mcp)
if not mcp:
return
try:
@@ -2866,9 +2980,13 @@ class Pipe:
)
chat_id = chat_ctx.get("chat_id") or "default"
# Determine effective MCP and cache settings
# Determine effective MCP settings
effective_mcp = user_valves.ENABLE_MCP_SERVER
effective_cache = user_valves.ENABLE_TOOL_CACHE
# P4: Chat tool_ids whitelist — extract once, reuse for both OpenAPI and MCP
chat_tool_ids = None
if __metadata__ and isinstance(__metadata__, dict):
chat_tool_ids = __metadata__.get("tool_ids") or None
user_ctx = await self._get_user_context(__user__, __event_call__, __request__)
user_lang = user_ctx["user_language"]
@@ -2879,7 +2997,6 @@ class Pipe:
debug_enabled=effective_debug,
token=effective_token,
enable_mcp=effective_mcp,
enable_cache=effective_cache,
__event_emitter__=__event_emitter__,
user_lang=user_lang,
)
@@ -3125,7 +3242,7 @@ class Pipe:
# Check MCP Servers
mcp_servers = self._parse_mcp_servers(
__event_call__, enable_mcp=effective_mcp, enable_cache=effective_cache
__event_call__, enable_mcp=effective_mcp, chat_tool_ids=chat_tool_ids
)
mcp_server_names = list(mcp_servers.keys()) if mcp_servers else []
if mcp_server_names:
@@ -3180,15 +3297,13 @@ class Pipe:
mcp_servers = self._parse_mcp_servers(
__event_call__,
enable_mcp=effective_mcp,
enable_cache=effective_cache,
chat_tool_ids=chat_tool_ids,
)
if mcp_servers:
resume_params["mcp_servers"] = mcp_servers
resume_params["available_tools"] = None
else:
resume_params["available_tools"] = (
[t.name for t in custom_tools] if custom_tools else None
)
# Always None: let CLI built-ins (bash etc.) remain available.
resume_params["available_tools"] = None
# Always inject the latest system prompt in 'replace' mode
# This handles both custom models and user-defined system messages
@@ -3274,7 +3389,7 @@ class Pipe:
is_admin=is_admin,
user_id=user_id,
enable_mcp=effective_mcp,
enable_cache=effective_cache,
chat_tool_ids=chat_tool_ids,
__event_call__=__event_call__,
)

View File

@@ -0,0 +1,127 @@
# 🚀 GitHub Copilot SDK Pipe v0.8.0: Conditional Tool Filtering & Publish Reliability 🎛️
**GitHub Copilot SDK Pipe v0.8.0** — A major control and reliability upgrade. This release introduces a four-priority tool permission system that makes tool access fully configurable per conversation, fixes file publishing across all storage backends, and ensures CLI built-in tools are never accidentally silenced.
---
## 📦 Quick Installation
- **GitHub Copilot SDK (Pipe)**: [Install v0.8.0](https://openwebui.com/posts/ce96f7b4-12fc-4ac3-9a01-875713e69359)
- **GitHub Copilot SDK (Filter)**: [Install v0.1.3](https://openwebui.com/posts/403a62ee-a596-45e7-be65-fab9cc249dd6)
---
## 🚀 What's New in v0.8.0
### 1. Conditional Tool Filtering (P1~P4) — The Headline Feature
This release introduces a **four-priority tool permission system** that gives you precise control over which tools are active in each conversation.
| Priority | Scope | Mechanism |
| :--- | :--- | :--- |
| **P1** | Global | Pipe-level Valve switches (`ENABLE_OPENWEBUI_TOOLS`, `ENABLE_OPENAPI_SERVER`, `ENABLE_MCP`) |
| **P2** | Server-level | Admin `config.enable` — disable a tool server from the OpenWebUI admin panel without changing code |
| **P3** | User-level | Per-user `UserValves` overrides |
| **P4** | Conversation-level | **Chat UI tool selection** — only tools the user explicitly checks in the chat input are activated |
**How P4 works**: OpenWebUI passes the user's currently selected tools as `tool_ids` in `__metadata__`. The plugin reads this whitelist and filters both **OpenWebUI tools** and **MCP servers** accordingly.
- **Default ON (No Selection)**: If you **do not select any tools** (leave the checkbox empty), the plugin considers all enabled tools active (provided the P1 global Valve is enabled). OpenWebUI tools, OpenAPI Servers, and MCP Servers are **all mounted by default**.
- **Whitelist Mode (Selection Active)**: Once you explicitly check at least one tool (e.g., only "web search"), **only your selected tools are activated**, and all other tools (including MCP servers) are filtered out.
```
User checks ✅ web-search, ✅ code-runner in Chat UI
chat_tool_ids = ["web-search", "server:code-runner"]
filtered tools = only the two checked tools
MCP servers not in the list are skipped entirely
```
**P2 admin control**: Each tool server entry in OpenWebUI now has a `config.enable` field. Setting it to `false` from the admin panel disables that server globally — no Valve edits required.
### 2. File Publish Reliability — All Storage Backends Fixed
The persistent `{"detail":"[ERROR: Error getting file content]"}` error when agents publish files has been fully resolved.
| Root Cause | Fix |
| :--- | :--- |
| Fallback path used `shutil.copy2` + manual DB write, which stored a local absolute path that S3-backed deployments cannot resolve | Fallback now calls `Storage.upload_file()` directly, auto-adapting to local/S3/GCS/Azure |
| HTML files were blocked by OpenWebUI's `ALLOWED_FILE_EXTENSIONS` check | Upload URL always includes `?process=false`, bypassing the content-type filter |
When the published artifact is an HTML file, the plugin also returns a direct-access HTML link for immediate opening/preview in chat.
All published links now follow a strict format: they must be relative paths beginning with `/api/v1/files/`.
`api/...` (without leading slash) and domain-prefixed absolute URLs are treated as invalid.
### 3. CLI Built-in Tools Always Available
`available_tools` is now always `None` (instead of filtering by loaded tool IDs), which means Copilot CLI built-in tools — such as `bash`, `create_file`, `read_file`, `list_directory` — are **always available by default**.
Previously, when no server tools were configured/loaded, the CLI could receive `available_tools=[]` and silently block all built-ins.
### 4. Publish Tool Always Injected
`publish_file_from_workspace` is injected into the tool list before the `ENABLE_OPENWEBUI_TOOLS` guard, so it is **never lost** even when all OpenWebUI tool loading is disabled via Valves.
### 5. Code Interpreter Warning (Not Disabled)
The built-in `code_interpreter` tool is **enabled** but operates in a remote, ephemeral sandbox.
A system prompt warning is now automatically injected to clarify its limitations:
> "The `code_interpreter` tool executes code in a remote, ephemeral environment. It cannot access files in your local workspace or persist changes."
### 6. Bug Fixes Summary
- **File publish backend mismatch**: Fixed object-storage publish failures (`Error getting file content`) caused by local-path fallback writes.
- **HTML artifact extension rejection**: Fixed HTML uploads being blocked by OpenWebUI extension processing by forcing `?process=false`.
- **Invalid artifact URL formats**: Fixed links occasionally generated as `api/...` or domain-prefixed URLs; output is now constrained to `/api/v1/files/...` relative paths.
- **CLI built-ins silently blocked**: Fixed built-ins becoming unavailable when no server tools were configured/loaded (which resulted in `available_tools=[]`); now default remains `None`.
- **Publish tool injection loss**: Fixed `publish_file_from_workspace` being omitted when `ENABLE_OPENWEBUI_TOOLS=False`.
### 7. Companion Files Filter Included in Pipe Guidance
The release documentation now embeds the key points of `GitHub Copilot SDK Files Filter` so users can configure the full workflow from a single page:
- It prevents default RAG pre-processing from consuming uploaded files before Pipe processing.
- It preserves raw binary access by moving uploads into `copilot_files`.
- v0.1.3 improvements included in guidance: BYOK model-id prefix matching + optional dual-channel debug logs.
---
## 🛠️ Key Capabilities
| Feature | Description |
| :--- | :--- |
| **Conditional Tool Filtering (P1~P4)** | Multi-priority permission system: global Valves → admin config.enable → user valves → Chat UI selection |
| **Universal Tool Protocol** | Native support for **MCP**, **OpenAPI**, and **OpenWebUI built-in tools** |
| **Native Tool Call UI** | Adapted to OpenWebUI's built-in tool call rendering |
| **Workspace Isolation** | Strict sandboxing for per-session data privacy and security |
| **Workspace Artifacts** | Agents generate files (Excel/CSV/HTML) with persistent download links |
| **Multi-storage Publishing** | File publish works across local disk, S3, GCS, and Azure backends |
| **11-Language Localization** | Auto-detected, native status messages for global users |
---
## 🔄 Migration Notes
- **No breaking changes** for existing users. The P4 whitelist is only active when the user has explicitly selected tools in the Chat UI; with no selection, all enabled tools are passed as before.
- Users on S3/GCS/Azure deployments who experienced file download failures can now publish normally without any additional configuration.
- `ENABLE_TOOL_CACHE` Valve has been removed. Tool server connections are now always read fresh from the database to avoid stale state across multiple workers. Use `MODEL_CACHE_TTL` for model list caching.
---
## 📥 Import Chat Templates
- [📥 Star Prediction Chat log](https://fu-jie.github.io/awesome-openwebui/plugins/pipes/star-prediction-chat.json)
- [📥 Video Processing Chat log](https://fu-jie.github.io/awesome-openwebui/plugins/pipes/video-processing-chat.json)
*Settings → Data → Import Chats.*
---
## 🔗 Resources
- **GitHub Repository**: [openwebui-extensions](https://github.com/Fu-Jie/openwebui-extensions)
- **Full Changelog**: [README.md](https://github.com/Fu-Jie/openwebui-extensions/blob/main/plugins/pipes/github-copilot-sdk/README.md)

View File

@@ -0,0 +1,131 @@
# 🚀 GitHub Copilot SDK Pipe v0.8.0:条件工具过滤 & 发布可靠性全面升级 🎛️
**GitHub Copilot SDK Pipe v0.8.0** — 一次重大的权限管控与稳定性升级。本次更新引入了四优先级工具权限体系,使工具访问可按每次对话精细配置;同时彻底修复了所有存储后端的文件发布问题,并确保 CLI 内置工具不再被意外屏蔽。
---
## 📦 快速安装
- **GitHub Copilot SDK (Pipe 插件)**: [安装 v0.8.0](https://openwebui.com/posts/ce96f7b4-12fc-4ac3-9a01-875713e69359)
- **GitHub Copilot SDK (Filter 插件)**: [安装 v0.1.3](https://openwebui.com/posts/403a62ee-a596-45e7-be65-fab9cc249dd6)
---
## 🚀 v0.8.0 更新内容
### 1. 条件工具过滤 (P1~P4) — 核心新特性
本次更新引入了**四优先级工具权限体系**,为您提供对每次对话中可用工具的精确控制。
| 优先级 | 作用域 | 机制描述 |
| :--- | :--- | :--- |
| **P1** | 全局 | Pipe 级 Valve 开关(`ENABLE_OPENWEBUI_TOOLS``ENABLE_OPENAPI_SERVER``ENABLE_MCP` |
| **P2** | 服务器级 | 管理员 `config.enable` — 无需修改代码,直接从 OpenWebUI 管理面板禁用某个工具服务器 |
| **P3** | 用户级 | 用户专属 `UserValves` 覆盖设置 |
| **P4** | 对话级 | **Chat UI 工具选择** — 只有用户在输入框明确勾选的工具才会在本次对话中激活 |
**P4 工作原理**OpenWebUI 会将用户当前选中的工具 ID 通过 `__metadata__` 中的 `tool_ids` 字段传入。插件读取该白名单并对 **OpenWebUI 工具**和 **MCP server** 同时进行过滤。
- **默认全开 (Default ON)**:如果您在 Chat UI 中**未勾选任何工具**(留空),只要 P1 全局 Valve 开启,插件就会默认启用**所有可用工具**。OpenWebUI 工具、OpenAPI Server 和 MCP Server 将默认全部挂载。
- **按需激活 (Whitelist Mode)**:一旦您在输入框主动勾选了至少一个工具(例如只选了“联网搜索”),则**只有您选中的工具会生效**,其他未选中的工具(包括 MCP 服务器)都将被过滤掉。
```
用户在 Chat UI 勾选 ✅ web-search✅ code-runner
chat_tool_ids = ["web-search", "server:code-runner"]
过滤后工具列表 = 仅包含上述两个工具
白名单外的 MCP server 将被完全跳过
```
**P2 管理员管控**OpenWebUI 中每个工具服务器条目现在都有 `config.enable` 字段。从管理面板将其设为 `false` 即可全局禁用该服务器,无需修改任何 Valve 配置。
### 2. 文件发布全面修复 — 适配所有存储后端
Agent 发布文件时持续出现的 `{"detail":"[ERROR: Error getting file content]"}` 错误已被彻底解决。
| 根本原因 | 修复方案 |
| :--- | :--- |
| 回退路径使用 `shutil.copy2` + 手动写入 DB导致 `file.path` 存储的是本地绝对路径S3 等对象存储后端无法解析 | 回退路径改为直接调用 `Storage.upload_file()`,自动适配 local/S3/GCS/Azure |
| HTML 文件被 OpenWebUI `ALLOWED_FILE_EXTENSIONS` 内容类型检查拦截 | 上传 URL 始终追加 `?process=false`,绕过文件类型限制 |
当发布产物为 HTML 文件时,插件还会返回一个可直接访问的 HTML 链接,便于在聊天中即时打开/预览。
所有发布链接现统一执行严格格式:必须是以 `/api/v1/files/` 开头的相对路径。
`api/...`(缺少前导 `/`)或拼接域名的绝对 URL 都视为无效格式。
### 3. CLI 内置工具始终可用
`available_tools` 现在统一设为 `None`(不再根据已加载的工具 ID 列表过滤),这意味着 Copilot CLI 内置工具 —— 如 `bash``create_file``read_file``list_directory` —— **默认始终可用**
此前,在未配置/未加载任何 server 工具时CLI 可能收到 `available_tools=[]`,导致所有内置工具被静默屏蔽。
### 4. 发布工具始终注入
`publish_file_from_workspace` 工具的注入逻辑提前到 `ENABLE_OPENWEBUI_TOOLS` 守卫之前,因此即使通过 Valve 关闭所有 OpenWebUI 工具加载,该工具也**不再丢失**。
### 5. 代码解释器警告(未禁用)
内置的 `code_interpreter` 工具保持**启用**状态,但运行在远程的临时沙箱中。
为了避免混淆,系统提示词中会自动注入一段警告说明:
> "The `code_interpreter` tool executes code in a remote, ephemeral environment. It cannot access files in your local workspace or persist changes."
请仅将其用于纯计算或逻辑验证,勿用于文件操作。
### 6. Bug 修复汇总
- **文件发布后端不匹配**:修复了对象存储场景下因本地路径回退写入导致的 `Error getting file content`
- **HTML 产物扩展名拦截**:修复了 OpenWebUI 扩展名处理导致 HTML 上传被拒的问题,统一强制 `?process=false`
- **产物链接格式错误**:修复了链接偶发被生成成 `api/...` 或带域名 URL 的问题,现统一限制为 `/api/v1/files/...` 相对路径。
- **CLI 内置工具被静默屏蔽**:修复了在未配置/未加载任何 server 工具时(最终出现 `available_tools=[]`)导致内置工具不可用的问题,默认保持 `None`
- **发布工具注入丢失**:修复了 `ENABLE_OPENWEBUI_TOOLS=False``publish_file_from_workspace` 被遗漏的问题。
### 7. 在 Pipe 文档中整合 Files Filter 指南
本次已将 `GitHub Copilot SDK Files Filter` 的核心说明整合进 Pipe 文档,用户可在单页完成完整配置:
- 阻止默认 RAG 在 Pipe 接手前抢先处理上传文件。
- 通过将上传文件移动到 `copilot_files` 保留原始二进制访问能力。
- 纳入 v0.1.3 重点BYOK 模型 ID 前缀匹配修复 + 可选双通道调试日志。
---
## 🛠️ 核心能力
| 特性 | 描述 |
| :--- | :--- |
| **条件工具过滤 (P1~P4)** | 多优先级权限体系:全局 Valve → 管理员 config.enable → 用户 Valve → Chat UI 选择 |
| **通用工具协议** | 原生支持 **MCP**、**OpenAPI** 以及 **OpenWebUI 内置工具** |
| **原生工具 UI** | 完美适配 OpenWebUI 内置的工具调用渲染 |
| **物理隔离工作区** | 为每个会话提供严格的沙箱环境,保护数据隐私与安全 |
| **工作区产物工具** | Agent 可生成文件Excel/CSV/HTML并提供持久化下载链接 |
| **多存储后端发布** | 文件发布兼容本地磁盘、S3、GCS 及 Azure 后端 |
| **11 国语言本地化** | 自动检测并显示原生语言的状态消息 |
---
## 🔄 迁移说明
- **无破坏性变更**。P4 白名单过滤仅在用户主动在 Chat UI 选择了工具时生效;未做任何选择时,所有已启用的工具照常传递给 Copilot。
- 使用 S3/GCS/Azure 存储后端、曾遭遇文件下载失败的用户,无需任何额外配置即可正常发布文件。
- `ENABLE_TOOL_CACHE` Valve 已移除。工具服务器连接现在始终从数据库实时读取,避免多 Worker 环境下的状态过时。模型列表缓存请使用 `MODEL_CACHE_TTL`
---
## 📥 导入对话模板
您可以下载并导入真实案例的 JSON 对话日志,直观查看模型如何调用工具:
- [📥 对话日志GitHub 增长预测](https://fu-jie.github.io/awesome-openwebui/plugins/pipes/star-prediction-chat.json)
- [📥 对话日志:视频高质量 GIF 转换](https://fu-jie.github.io/awesome-openwebui/plugins/pipes/video-processing-chat.json)
*导入方式:前往 `设置 → 数据 → 导入对话`。*
---
## 🔗 相关资源
- **GitHub 仓库**: [openwebui-extensions](https://github.com/Fu-Jie/openwebui-extensions)
- **完整变更日志**: [README_CN.md](https://github.com/Fu-Jie/openwebui-extensions/blob/main/plugins/pipes/github-copilot-sdk/README_CN.md)