feat: Implement configurable OpenWebUI base URL for deployment scripts and update documentation.

This commit is contained in:
fujie
2026-03-11 23:32:00 +08:00
parent 5fe66a5803
commit fdf95a2825
13 changed files with 203 additions and 128 deletions

1
.gitignore vendored
View File

@@ -142,3 +142,4 @@ logs/
# OpenWebUI specific
# Add any specific ignores for OpenWebUI plugins if needed
.git-worktrees/
plugins/filters/auth_model_info/

View File

@@ -1886,19 +1886,9 @@ class Filter:
"""
Check if compression should be skipped.
Returns True if:
1. The base model includes 'copilot_sdk'
"""
# Check if base model includes copilot_sdk
if __model__:
base_model_id = __model__.get("base_model_id", "")
if "copilot_sdk" in base_model_id.lower():
return True
# Also check model in body
model_id = body.get("model", "")
if "copilot_sdk" in model_id.lower():
if body.get("is_copilot_model", False):
return True
return False
async def inlet(

View File

@@ -114,6 +114,7 @@ class Filter:
# Check if it's a Copilot model
is_copilot_model = self._is_copilot_model(current_model)
body["is_copilot_model"] = is_copilot_model
await self._emit_debug_log(
__event_emitter__,

View File

@@ -9,6 +9,7 @@ This directory contains automated scripts for deploying plugins in development t
1. **OpenWebUI Running**: Make sure OpenWebUI is running locally (default `http://localhost:3000`)
2. **API Key**: You need a valid OpenWebUI API key
3. **Environment File**: Create a `.env` file in this directory containing your API key:
```
api_key=sk-xxxxxxxxxxxxx
```
@@ -42,12 +43,14 @@ python deploy_filter.py --list
Used to deploy Filter-type plugins (such as message filtering, context compression, etc.).
**Key Features**:
- ✅ Auto-extracts metadata from Python files (version, author, description, etc.)
- ✅ Attempts to update existing plugins, creates if not found
- ✅ Supports multiple Filter plugin management
- ✅ Detailed error messages and connection diagnostics
**Usage**:
```bash
# Deploy async_context_compression (default)
python deploy_filter.py
@@ -62,6 +65,7 @@ python deploy_filter.py -l
```
**Workflow**:
1. Load API key from `.env`
2. Find target Filter plugin directory
3. Read Python source file
@@ -76,6 +80,7 @@ python deploy_filter.py -l
Used to deploy Pipe-type plugins (such as GitHub Copilot SDK).
**Usage**:
```bash
python deploy_pipe.py
```
@@ -101,6 +106,7 @@ Create a dedicated long-term API key in OpenWebUI Settings for deployment purpos
**Cause**: OpenWebUI is not running or port is different
**Solution**:
- Make sure OpenWebUI is running
- Check which port OpenWebUI is actually listening on (usually 3000)
- Edit the URL in the script if needed
@@ -110,6 +116,7 @@ Create a dedicated long-term API key in OpenWebUI Settings for deployment purpos
**Cause**: `.env` file was not created
**Solution**:
```bash
echo "api_key=sk-your-api-key-here" > .env
```
@@ -119,6 +126,7 @@ echo "api_key=sk-your-api-key-here" > .env
**Cause**: Filter directory name is incorrect
**Solution**:
```bash
# List all available Filters
python deploy_filter.py --list
@@ -129,6 +137,7 @@ python deploy_filter.py --list
**Cause**: API key is invalid or expired
**Solution**:
1. Verify your API key is valid
2. Generate a new API key
3. Update the `.env` file
@@ -178,6 +187,7 @@ python deploy_filter.py async-context-compression
## Security Considerations
⚠️ **Important**:
- ✅ Add `.env` file to `.gitignore` (avoid committing sensitive info)
- ✅ Never commit API keys to version control
- ✅ Use only on trusted networks

View File

@@ -7,6 +7,7 @@ Added a complete local deployment toolchain for the `async_context_compression`
## 📋 New Files
### 1. **deploy_filter.py** — Filter Plugin Deployment Script
- **Location**: `scripts/deploy_filter.py`
- **Function**: Auto-deploy Filter-type plugins to local OpenWebUI instance
- **Features**:
@@ -19,6 +20,7 @@ Added a complete local deployment toolchain for the `async_context_compression`
- **Code Lines**: ~300
### 2. **DEPLOYMENT_GUIDE.md** — Complete Deployment Guide
- **Location**: `scripts/DEPLOYMENT_GUIDE.md`
- **Contents**:
- Prerequisites and quick start
@@ -28,6 +30,7 @@ Added a complete local deployment toolchain for the `async_context_compression`
- Step-by-step workflow examples
### 3. **QUICK_START.md** — Quick Reference Card
- **Location**: `scripts/QUICK_START.md`
- **Contents**:
- One-line deployment command
@@ -37,6 +40,7 @@ Added a complete local deployment toolchain for the `async_context_compression`
- CI/CD integration examples
### 4. **test_deploy_filter.py** — Unit Test Suite
- **Location**: `tests/scripts/test_deploy_filter.py`
- **Test Coverage**:
- ✅ Filter file discovery (3 tests)
@@ -138,6 +142,7 @@ openwebui_id: b1655bc8-6de9-4cad-8cb5-a6f7829a02ce
```
**Supported Metadata Fields**:
- `title` — Filter display name ✅
- `id` — Unique identifier ✅
- `author` — Author name ✅
@@ -335,17 +340,20 @@ Metadata Extraction and Delivery
### Debugging Tips
1. **Enable Verbose Logging**:
```bash
python deploy_filter.py 2>&1 | tee deploy.log
```
2. **Test API Connection**:
```bash
curl -X GET http://localhost:3000/api/v1/functions \
-H "Authorization: Bearer $API_KEY"
```
3. **Verify .env File**:
```bash
grep "api_key=" scripts/.env
```

View File

@@ -73,12 +73,14 @@ python deploy_async_context_compression.py
```
**Features**:
- ✅ Optimized specifically for async_context_compression
- ✅ Clear deployment steps and confirmation
- ✅ Friendly error messages
- ✅ Shows next steps after successful deployment
**Sample Output**:
```
======================================================================
🚀 Deploying Async Context Compression Filter Plugin
@@ -117,6 +119,7 @@ python deploy_filter.py --list
```
**Features**:
- ✅ Generic Filter deployment tool
- ✅ Supports multiple plugins
- ✅ Auto metadata extraction
@@ -142,6 +145,7 @@ python deploy_tool.py openwebui-skills-manager
```
**Features**:
- ✅ Supports Tools plugin deployment
- ✅ Auto-detects `Tools` class definition
- ✅ Smart update/create logic
@@ -290,6 +294,7 @@ git status # should not show .env
```
**Solution**:
```bash
# 1. Check if OpenWebUI is running
curl http://localhost:3000
@@ -309,6 +314,7 @@ curl http://localhost:3000
```
**Solution**:
```bash
echo "api_key=sk-your-api-key" > .env
cat .env # verify file created
@@ -321,6 +327,7 @@ cat .env # verify file created
```
**Solution**:
```bash
# List all available Filters
python deploy_filter.py --list
@@ -337,6 +344,7 @@ python deploy_filter.py async-context-compression
```
**Solution**:
```bash
# 1. Verify API key is correct
grep "api_key=" .env
@@ -370,7 +378,7 @@ python deploy_async_context_compression.py
### Method 2: Verify in OpenWebUI
1. Open OpenWebUI: http://localhost:3000
1. Open OpenWebUI: <http://localhost:3000>
2. Go to Settings → Filters
3. Check if 'Async Context Compression' is listed
4. Verify version number is correct (should be latest)
@@ -380,6 +388,7 @@ python deploy_async_context_compression.py
1. Open a new conversation
2. Enable 'Async Context Compression' Filter
3. Have multiple-turn conversation and verify compression/summarization works
## 💡 Advanced Usage
### Automated Deploy & Test
@@ -473,4 +482,3 @@ Newly created deployment-related files:
**Last Updated**: 2026-03-09
**Script Status**: ✅ Ready for production
**Test Coverage**: 10/10 passed ✅

View File

@@ -5,6 +5,7 @@
**Yes, re-deploying automatically updates the plugin!**
The deployment script uses a **smart two-stage strategy**:
1. 🔄 **Try UPDATE First** (if plugin exists)
2. 📝 **Auto CREATE** (if update fails — plugin doesn't exist)
@@ -54,6 +55,7 @@ if response.status_code == 200:
```
**What Happens**:
- Send **POST** to `/api/v1/functions/id/{filter_id}/update`
- If returns **HTTP 200**, plugin exists and update succeeded
- Includes:
@@ -84,6 +86,7 @@ if response.status_code != 200:
```
**What Happens**:
- If update fails (HTTP ≠ 200), auto-attempt create
- Send **POST** to `/api/v1/functions/create`
- Uses **same payload** (code, metadata identical)
@@ -103,6 +106,7 @@ $ python deploy_async_context_compression.py
```
**What Happens**:
1. Try UPDATE → fails (HTTP 404 — plugin doesn't exist)
2. Auto-try CREATE → succeeds (HTTP 200)
3. Plugin created in OpenWebUI
@@ -121,6 +125,7 @@ $ python deploy_async_context_compression.py
```
**What Happens**:
1. Read modified code
2. Try UPDATE → succeeds (HTTP 200 — plugin exists)
3. Plugin in OpenWebUI updated to latest code
@@ -147,6 +152,7 @@ $ python deploy_async_context_compression.py
```
**Characteristics**:
- 🚀 Each update takes only 5 seconds
- 📝 Each is an incremental update
- ✅ No need to restart OpenWebUI
@@ -181,11 +187,13 @@ version: 1.3.0
```
**Each deployment**:
1. Script reads version from docstring
2. Sends this version in manifest to OpenWebUI
3. If you change version in code, deployment updates to new version
**Best Practice**:
```bash
# 1. Modify code
vim async_context_compression.py
@@ -300,6 +308,7 @@ Usually **not needed** because:
4. ✅ Failures auto-rollback
但如果真的需要控制,可以:
- 手动修改脚本 (修改 `deploy_filter.py`)
- 或分别使用 UPDATE/CREATE 的具体 API 端点
@@ -323,6 +332,7 @@ Usually **not needed** because:
### Q: 可以同时部署多个插件吗?
**可以!**
```bash
python deploy_filter.py async-context-compression
python deploy_filter.py folder-memory
@@ -337,6 +347,7 @@ python deploy_filter.py context_enhancement_filter
---
**总结**: 部署脚本的更新机制完全自动化,开发者只需修改代码,每次运行 `deploy_async_context_compression.py` 就会自动:
1. ✅ 创建(第一次)或更新(后续)插件
2. ✅ 从代码提取最新的元数据和版本号
3. ✅ 立即生效,无需重启 OpenWebUI

View File

@@ -49,6 +49,31 @@ def _load_api_key() -> str:
raise ValueError("api_key not found in .env file.")
def _load_openwebui_base_url() -> str:
"""Load OpenWebUI base URL from .env file or environment.
Checks in order:
1. OPENWEBUI_BASE_URL in .env
2. OPENWEBUI_BASE_URL environment variable
3. Default to http://localhost:3000
"""
if ENV_FILE.exists():
for line in ENV_FILE.read_text(encoding="utf-8").splitlines():
line = line.strip()
if line.startswith("OPENWEBUI_BASE_URL="):
url = line.split("=", 1)[1].strip()
if url:
return url
# Try environment variable
url = os.environ.get("OPENWEBUI_BASE_URL")
if url:
return url
# Default
return "http://localhost:3000"
def _find_filter_file(filter_name: str) -> Optional[Path]:
"""Find the main Python file for a filter.
@@ -126,7 +151,9 @@ def _build_filter_payload(
filter_id = metadata.get("id", filter_name).replace("-", "_")
title = metadata.get("title", filter_name)
author = metadata.get("author", "Fu-Jie")
author_url = metadata.get("author_url", "https://github.com/Fu-Jie/openwebui-extensions")
author_url = metadata.get(
"author_url", "https://github.com/Fu-Jie/openwebui-extensions"
)
funding_url = metadata.get("funding_url", "https://github.com/open-webui")
description = metadata.get("description", f"Filter plugin: {title}")
version = metadata.get("version", "1.0.0")
@@ -211,11 +238,13 @@ def deploy_filter(filter_name: str = DEFAULT_FILTER) -> bool:
}
# 6. Send update request
update_url = "http://localhost:3000/api/v1/functions/id/{}/update".format(filter_id)
create_url = "http://localhost:3000/api/v1/functions/create"
base_url = _load_openwebui_base_url()
update_url = "{}/api/v1/functions/id/{}/update".format(base_url, filter_id)
create_url = "{}/api/v1/functions/create".format(base_url)
print(f"📦 Deploying filter '{title}' (version {version})...")
print(f" File: {file_path}")
print(f" Target: {base_url}")
try:
# Try update first
@@ -247,7 +276,9 @@ def deploy_filter(filter_name: str = DEFAULT_FILTER) -> bool:
print(f"✅ Successfully created '{title}' filter!")
return True
else:
print(f"❌ Failed to update or create. Status: {res_create.status_code}")
print(
f"❌ Failed to update or create. Status: {res_create.status_code}"
)
try:
error_msg = res_create.json()
print(f" Error: {error_msg}")
@@ -256,9 +287,8 @@ def deploy_filter(filter_name: str = DEFAULT_FILTER) -> bool:
return False
except requests.exceptions.ConnectionError:
print(
"❌ Connection error: Could not reach OpenWebUI at localhost:3000"
)
base_url = _load_openwebui_base_url()
print(f"❌ Connection error: Could not reach OpenWebUI at {base_url}")
print(" Make sure OpenWebUI is running and accessible.")
return False
except requests.exceptions.Timeout:
@@ -272,7 +302,11 @@ def deploy_filter(filter_name: str = DEFAULT_FILTER) -> bool:
def list_filters() -> None:
"""List all available filters."""
print("📋 Available filters:")
filters = [d.name for d in FILTERS_DIR.iterdir() if d.is_dir() and not d.name.startswith("_")]
filters = [
d.name
for d in FILTERS_DIR.iterdir()
if d.is_dir() and not d.name.startswith("_")
]
if not filters:
print(" (No filters found)")

View File

@@ -76,8 +76,7 @@ def _get_base_url() -> str:
if not base_url:
raise ValueError(
f"Missing url. Please create {ENV_FILE} with: "
"url=http://localhost:3000"
f"Missing url. Please create {ENV_FILE} with: " "url=http://localhost:3000"
)
return base_url.rstrip("/")
@@ -141,7 +140,9 @@ def _build_tool_payload(
tool_id = metadata.get("id", tool_name).replace("-", "_")
title = metadata.get("title", tool_name)
author = metadata.get("author", "Fu-Jie")
author_url = metadata.get("author_url", "https://github.com/Fu-Jie/openwebui-extensions")
author_url = metadata.get(
"author_url", "https://github.com/Fu-Jie/openwebui-extensions"
)
funding_url = metadata.get("funding_url", "https://github.com/open-webui")
description = metadata.get("description", f"Tool plugin: {title}")
version = metadata.get("version", "1.0.0")
@@ -263,7 +264,9 @@ def deploy_tool(tool_name: str = DEFAULT_TOOL) -> bool:
print(f"✅ Successfully created '{title}' tool!")
return True
else:
print(f"❌ Failed to update or create. Status: {res_create.status_code}")
print(
f"❌ Failed to update or create. Status: {res_create.status_code}"
)
try:
error_msg = res_create.json()
print(f" Error: {error_msg}")
@@ -272,9 +275,7 @@ def deploy_tool(tool_name: str = DEFAULT_TOOL) -> bool:
return False
except requests.exceptions.ConnectionError:
print(
"❌ Connection error: Could not reach OpenWebUI at {base_url}"
)
print("❌ Connection error: Could not reach OpenWebUI at {base_url}")
print(" Make sure OpenWebUI is running and accessible.")
return False
except requests.exceptions.Timeout:
@@ -288,7 +289,9 @@ def deploy_tool(tool_name: str = DEFAULT_TOOL) -> bool:
def list_tools() -> None:
"""List all available tools."""
print("📋 Available tools:")
tools = [d.name for d in TOOLS_DIR.iterdir() if d.is_dir() and not d.name.startswith("_")]
tools = [
d.name for d in TOOLS_DIR.iterdir() if d.is_dir() and not d.name.startswith("_")
]
if not tools:
print(" (No tools found)")

View File

@@ -187,9 +187,7 @@ def build_payload(candidate: PluginCandidate) -> Dict[str, object]:
manifest = dict(candidate.metadata)
manifest.setdefault("title", candidate.title)
manifest.setdefault("author", "Fu-Jie")
manifest.setdefault(
"author_url", "https://github.com/Fu-Jie/openwebui-extensions"
)
manifest.setdefault("author_url", "https://github.com/Fu-Jie/openwebui-extensions")
manifest.setdefault("funding_url", "https://github.com/open-webui")
manifest.setdefault(
"description", f"{candidate.plugin_type.title()} plugin: {candidate.title}"
@@ -233,7 +231,9 @@ def build_api_urls(base_url: str, candidate: PluginCandidate) -> Tuple[str, str]
)
def discover_plugins(plugin_types: Sequence[str]) -> Tuple[List[PluginCandidate], List[Tuple[Path, str]]]:
def discover_plugins(
plugin_types: Sequence[str],
) -> Tuple[List[PluginCandidate], List[Tuple[Path, str]]]:
candidates: List[PluginCandidate] = []
skipped: List[Tuple[Path, str]] = []
@@ -344,7 +344,9 @@ def print_skipped_summary(skipped: Sequence[Tuple[Path, str]]) -> None:
for _, reason in skipped:
counts[reason] = counts.get(reason, 0) + 1
summary = ", ".join(f"{reason}: {count}" for reason, count in sorted(counts.items()))
summary = ", ".join(
f"{reason}: {count}" for reason, count in sorted(counts.items())
)
print(f"Skipped {len(skipped)} files ({summary}).")
@@ -421,7 +423,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
failed_candidates.append(candidate)
print(f" [FAILED] {message}")
print(f"\n" + "="*80)
print(f"\n" + "=" * 80)
print(
f"Finished: {success_count}/{len(candidates)} plugins installed successfully."
)
@@ -433,7 +435,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
print(f" → Check the error message above")
print()
print("="*80)
print("=" * 80)
return 0 if success_count == len(candidates) else 1

View File

@@ -9,13 +9,14 @@ local deployment are present and functional.
import sys
from pathlib import Path
def main():
"""Check all deployment tools are ready."""
base_dir = Path(__file__).parent.parent
print("\n" + "="*80)
print("\n" + "=" * 80)
print("✨ Async Context Compression Local Deployment Tools — Verification Status")
print("="*80 + "\n")
print("=" * 80 + "\n")
files_to_check = {
"🐍 Python Scripts": [
@@ -50,17 +51,17 @@ def main():
if exists and file_path.endswith(".py"):
size = full_path.stat().st_size
lines = len(full_path.read_text().split('\n'))
lines = len(full_path.read_text().split("\n"))
print(f" └─ [{size} bytes, ~{lines} lines]")
if not exists:
all_exist = False
print("\n" + "="*80)
print("\n" + "=" * 80)
if all_exist:
print("✅ All deployment tool files are ready!")
print("="*80 + "\n")
print("=" * 80 + "\n")
print("🚀 Quick Start (3 ways):\n")
@@ -83,7 +84,7 @@ def main():
print(" python deploy_filter.py folder-memory")
print()
print("="*80 + "\n")
print("=" * 80 + "\n")
print("📚 Documentation References:\n")
print(" • Quick Start: scripts/QUICK_START.md")
print(" • Complete Guide: scripts/DEPLOYMENT_GUIDE.md")
@@ -92,11 +93,11 @@ def main():
print(" • Test Coverage: pytest tests/scripts/test_deploy_filter.py -v")
print()
print("="*80 + "\n")
print("=" * 80 + "\n")
return 0
else:
print("❌ Some files are missing!")
print("="*80 + "\n")
print("=" * 80 + "\n")
return 1

View File

@@ -66,7 +66,7 @@ def test_build_payload_uses_native_tool_shape_for_tools():
"description": "Demo tool description",
"openwebui_id": "12345678-1234-1234-1234-123456789abc",
},
content='class Tools:\n pass\n',
content="class Tools:\n pass\n",
function_id="demo_tool",
)
@@ -79,7 +79,7 @@ def test_build_payload_uses_native_tool_shape_for_tools():
"description": "Demo tool description",
"manifest": {},
},
"content": 'class Tools:\n pass\n',
"content": "class Tools:\n pass\n",
"access_grants": [],
}
@@ -89,7 +89,7 @@ def test_build_api_urls_uses_tool_endpoints_for_tools():
plugin_type="tool",
file_path=Path("plugins/tools/demo/demo_tool.py"),
metadata={"title": "Demo Tool"},
content='class Tools:\n pass\n',
content="class Tools:\n pass\n",
function_id="demo_tool",
)
@@ -101,7 +101,9 @@ def test_build_api_urls_uses_tool_endpoints_for_tools():
assert create_url == "http://localhost:3000/api/v1/tools/create"
def test_discover_plugins_only_returns_supported_openwebui_plugins(tmp_path, monkeypatch):
def test_discover_plugins_only_returns_supported_openwebui_plugins(
tmp_path, monkeypatch
):
actions_dir = tmp_path / "plugins" / "actions"
filters_dir = tmp_path / "plugins" / "filters"
pipes_dir = tmp_path / "plugins" / "pipes"
@@ -110,7 +112,9 @@ def test_discover_plugins_only_returns_supported_openwebui_plugins(tmp_path, mon
write_plugin(actions_dir / "flash-card" / "flash_card.py", PLUGIN_HEADER)
write_plugin(actions_dir / "flash-card" / "flash_card_cn.py", PLUGIN_HEADER)
write_plugin(actions_dir / "infographic" / "verify_generation.py", PLUGIN_HEADER)
write_plugin(filters_dir / "missing-id" / "missing_id.py", '"""\ntitle: Missing ID\n"""\n')
write_plugin(
filters_dir / "missing-id" / "missing_id.py", '"""\ntitle: Missing ID\n"""\n'
)
write_plugin(pipes_dir / "sdk" / "github_copilot_sdk.py", PLUGIN_HEADER)
write_plugin(tools_dir / "skills" / "openwebui_skills_manager.py", PLUGIN_HEADER)
@@ -150,7 +154,9 @@ def test_discover_plugins_only_returns_supported_openwebui_plugins(tmp_path, mon
("class Action:\n pass\n", "missing plugin header"),
],
)
def test_discover_plugins_reports_missing_metadata(tmp_path, monkeypatch, header, expected_reason):
def test_discover_plugins_reports_missing_metadata(
tmp_path, monkeypatch, header, expected_reason
):
action_dir = tmp_path / "plugins" / "actions"
plugin_file = action_dir / "demo" / "demo.py"
write_plugin(plugin_file, header)