Initial commit

This commit is contained in:
alook
2026-08-18 10:03:35 +08:00
commit 8e4424b82a
11 changed files with 1524 additions and 0 deletions
+15
View File
@@ -0,0 +1,15 @@
CLIPROXY_API_KEY=replace-with-the-key-from-cliproxyapi-config
GEMINI_API_KEY=replace-with-your-gemini-api-key
JUPYTER_TOKEN=replace-with-a-strong-random-token
WEBUI_AUTH=true
ENABLE_LOGIN_FORM=true
ENABLE_SIGNUP=false
ENABLE_OAUTH_SIGNUP=true
DEFAULT_USER_ROLE=user
GOOGLE_CLIENT_ID=replace-with-your-google-client-id
GOOGLE_CLIENT_SECRET=replace-with-your-google-client-secret
GOOGLE_REDIRECT_URI=http://localhost:3000/oauth/google/callback
GOOGLE_OAUTH_SCOPE=openid email profile
OAUTH_ALLOWED_DOMAINS=example.com
+6
View File
@@ -0,0 +1,6 @@
.DS_Store
.env
cliproxyapi/config.yaml
cliproxyapi/auths/
cliproxyapi/logs/
litellm/gen-lang-client-*.json
+389
View File
@@ -0,0 +1,389 @@
# AI Stack
这个项目用 Docker Compose 组合了三个服务:
- LiteLLM:作为统一的模型代理层,对外提供 OpenAI 兼容接口
- Open WebUI:作为 Web 端聊天界面,直接连接 LiteLLM
- Jupyter:作为 Open WebUI Code Interpreter 的 Python 执行环境,用于生成 Excel 等文件
当前仓库的核心目标,是基于 Gemini 系列模型快速搭一套可本地运行的 AI 网关与聊天界面。
## 目录结构
- docker-compose.yml:服务编排文件
- litellm/config.yamlLiteLLM 模型映射与回退策略配置
- .env:本地环境变量文件,至少需要配置 Gemini API Key
- app/:预留目录,当前未使用
- open_webui/Open WebUI 的 Windows 服务配置目录
- install.ps1Windows 服务安装/卸载/启停脚本(WinSW)
- litellm/litellm-service.xmlLiteLLM 的 WinSW 服务配置
- open_webui/openwebui-service.xmlOpen WebUI 的 WinSW 服务配置
## 服务说明
### 1. LiteLLM
LiteLLM 容器暴露 4000 端口,对外提供 OpenAI 兼容接口。
当前配置特点:
- 通过环境变量 GEMINI_API_KEY 访问 Gemini 模型
- 读取 litellm/config.yaml 中定义的模型别名
- 为部分高阶模型配置了 fallback,当主模型不可用时自动降级到可用模型
- 容器内默认带有 HTTP_PROXY、HTTPS_PROXY、ALL_PROXY 配置,适合需要代理访问外网模型接口的环境
### 2. Open WebUI
Open WebUI 容器映射到本机 3000 端口。
当前配置特点:
- 通过 OPENAI_API_BASE_URL 指向 LiteLLM 的 OpenAI 兼容接口
- OPENAI_API_KEY 使用占位值 dummy,仅用于满足 Open WebUI 接口配置要求
- WEBUI_AUTH=false,默认关闭登录认证,适合内网或本地开发环境
- 使用 Docker Volume 持久化 WebUI 数据
- 已连接 Jupyter Code Interpreter,可让模型运行 Python 生成 .xlsx 等文件
### 3. Jupyter
Jupyter 只在 Docker 内网中提供给 Open WebUI 使用,不暴露到宿主机端口。
当前用途:
- 为 Open WebUI 的 Code Interpreter 提供 Python 执行环境
- 支持通过 pandas/openpyxl 等库生成 Excel 文件
- 使用 Docker Volume 持久化工作目录
在 Open WebUI 中导出 Excel 时,可以这样提问:
```text
请使用 Code Interpreter 生成一个 Excel 文件,包含以下字段:姓名、部门、金额,并提供下载链接。
```
## 已配置模型
LiteLLM 当前已配置以下 Gemini 模型别名:
- gemini-3-pro-preview
- gemini-3-flash-preview
- gemini-3.1-pro-preview
- gemini-3.1-flash-lite-preview
- gemini-2.5-pro
- gemini-2.5-flash
- gemini-2.5-flash-lite
- gemini-2.0-flash
- gemini-2.0-flash-001
- gemini-2.0-flash-lite
- gemini-flash-latest
- gemini-flash-lite-latest
- gemini-pro-latest
其中部分模型配置了自动回退策略:
- gemini-pro-latest 回退到 gemini-2.5-flash、gemini-flash-latest
- gemini-3.1-pro-preview 回退到 gemini-2.5-flash、gemini-flash-latest
- gemini-3-pro-preview 回退到 gemini-2.5-flash、gemini-flash-latest
## 运行前准备
需要具备以下环境:
- Docker
- Docker Compose
- 可用的 Gemini API Key
建议在 .env 中配置环境变量:
```env
GEMINI_API_KEY=你的实际密钥
```
如果当前网络环境访问 Gemini 需要代理,可以保留 docker-compose.yml 中的代理配置;如果不需要,应按实际环境修改或删除对应的代理地址。
## 启动方式
在项目根目录执行:
```bash
docker compose up -d
```
查看运行状态:
```bash
docker compose ps
```
查看日志:
```bash
docker compose logs -f
```
停止服务:
```bash
docker compose down
```
## Windows 下使用 Python 直接运行
如果不想使用 Docker,也可以在 Windows 下通过 Python 分别启动 LiteLLM 和 Open WebUI。
建议环境:
- Windows 10 或 11
- Python 3.11
- PowerShell
### 1. 创建虚拟环境并安装依赖
在项目根目录打开 PowerShell,执行:
```powershell
py -3.11 -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
pip install "litellm[proxy]" open-webui
```
如果 PowerShell 默认禁止脚本执行,可以先在当前会话临时放开:
```powershell
Set-ExecutionPolicy -Scope Process Bypass
```
### 2. 启动 LiteLLM
在第一个 PowerShell 窗口中执行:
```powershell
.\.venv\Scripts\Activate.ps1
$env:GEMINI_API_KEY="你的实际密钥"
# 如果访问 Gemini 需要代理,可以按实际环境设置
$env:HTTP_PROXY="http://127.0.0.1:7890"
$env:HTTPS_PROXY="http://127.0.0.1:7890"
$env:ALL_PROXY="http://127.0.0.1:7890"
litellm --config .\litellm\config.yaml --port 4000
```
说明:
- litellm/config.yaml 可以直接复用当前仓库里的模型配置
- 如果本机不需要代理,可以删除上面的代理环境变量
- 启动成功后,LiteLLM 默认监听 <http://127.0.0.1:4000>
### 3. 启动 Open WebUI
在第二个 PowerShell 窗口中执行:
```powershell
.\.venv\Scripts\Activate.ps1
$env:OPENAI_API_BASE_URL="http://127.0.0.1:4000/v1"
$env:OPENAI_API_KEY="dummy"
$env:WEBUI_AUTH="false"
$env:DATA_DIR="$PWD\.data\open-webui"
$env:PORT="3000"
open-webui serve
```
说明:
- OPENAI_API_BASE_URL 指向本地启动的 LiteLLM
- OPENAI_API_KEY 保持 dummy 即可,主要用于兼容 OpenAI 风格配置
- DATA_DIR 用于持久化 Open WebUI 本地数据
- PORT 设置为 3000,这样和 Docker 方式下的访问地址保持一致
### 4. 访问服务
启动完成后可通过以下地址访问:
- Open WebUI<http://127.0.0.1:3000>
- LiteLLM<http://127.0.0.1:4000>
- LiteLLM OpenAI 兼容接口:<http://127.0.0.1:4000/v1>
### 5. 停止服务
分别在两个 PowerShell 窗口中按 Ctrl+C 即可停止。
### 6. 常见问题
- 如果 open-webui 命令不存在,先确认虚拟环境已激活,并重新执行依赖安装
- 如果 LiteLLM 启动失败,优先检查 GEMINI_API_KEY 和 config.yaml 路径是否正确
- 如果 Open WebUI 页面打开但没有模型,优先检查 LiteLLM 是否已在 4000 端口正常启动
## Windows 下使用 WinSW 注册为系统服务
适用于需要长期稳定运行的场景,解决命令窗口关掉就停止、异常不自动恢复的问题。
WinSW 把 LiteLLM 和 Open WebUI 注册为 Windows Service,具备以下能力:
- 开机自动启动
- 进程异常退出后自动重启
- 无需保持终端窗口
- 日志自动滚动,不占满磁盘
- 可以在服务管理器(services.msc)里统一查看和管理
### 1. 准备工作
确认以下条件已满足:
- Python 虚拟环境已经创建,litellm 和 open-webui 已安装
- Python 虚拟环境目录建议为项目根目录下的 `venv`(可按需调整)
- 用管理员权限打开 PowerShell
### 2. 下载 WinSW
从官方 GitHub 下载最新版:
<https://github.com/winsw/winsw/releases/latest>
下载 `WinSW-x64.exe`,重命名为 `WinSW.exe`,放到项目根目录(与 `install.ps1` 同级)。
### 3. 修改配置文件
按实际情况修改这两个文件:
- `litellm/litellm-service.xml`:修改 GEMINI_API_KEY
- `open_webui/openwebui-service.xml`:按需调整端口和数据目录
关键字段说明:
- `<executable>`:默认使用 `%BASE%` 相对路径(例如 `%BASE%\\..\\venv\\Scripts\\litellm.exe`),复制到任意目录仍可运行
- `<env name="GEMINI_API_KEY" value="..."/>`:填写实际密钥
- `<logpath>`:日志输出目录,目录不存在时安装脚本会自动创建
说明:`%BASE%` 是 WinSW 内置变量,表示当前 WinSW.exe 所在目录。
### 4. 安装服务
在管理员 PowerShell 中,切换到项目根目录执行:
```powershell
cd 你的项目目录
Set-ExecutionPolicy -Scope Process Bypass
.\install.ps1
```
安装成功后会自动启动两个服务,并输出访问地址。
### 5. 日常管理
```powershell
# 查看服务状态
.\install.ps1 -Status
# 停止服务
.\install.ps1 -Stop
# 启动服务
.\install.ps1 -Start
# 卸载服务
.\install.ps1 -Uninstall
```
也可以通过 Windows 服务管理器(`Win+R` 输入 `services.msc`)可视化管理,
服务名称分别为 `AI Stack - LiteLLM``AI Stack - Open WebUI`
### 6. 日志位置
- LiteLLM 日志:`项目根目录\\logs\\litellm\\`
- Open WebUI 日志:`项目根目录\\logs\\openwebui\\`
日志文件为滚动模式,单文件超过 10MB 自动轮转,最多保留 5 个历史文件。
### 7. 修改配置后更新服务
修改 XML 文件后无需卸载重装,执行 refresh 即可:
```powershell
.\litellm\WinSW.exe refresh .\litellm\litellm-service.xml
.\open_webui\WinSW.exe refresh .\open_webui\openwebui-service.xml
```
## 访问入口
- Open WebUI<http://localhost:3000>
- LiteLLM API<http://localhost:4000>
- LiteLLM OpenAI 兼容接口前缀:<http://localhost:4000/v1>
## 使用方式
### 在 Open WebUI 中使用
启动后直接访问 Open WebUI 页面即可,通过 Web 界面选择 LiteLLM 暴露出来的模型进行对话。
### 通过 OpenAI 兼容接口调用
可以把 LiteLLM 当作 OpenAI 兼容网关使用,只需要把 Base URL 指向本机的 /v1 接口。
示例:
```bash
curl http://localhost:4000/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer dummy" \
-d '{
"model": "gemini-2.5-flash",
"messages": [
{"role": "user", "content": "你好"}
]
}'
```
说明:LiteLLM 实际调用 Gemini 时使用的是服务端环境变量中的 GEMINI_API_KEY;示例中的 Bearer dummy 只是为了满足 OpenAI 风格客户端的请求格式。
## 配置说明
### 模型配置
LiteLLM 的模型配置位于 litellm/config.yaml,可以按以下方式扩展:
- 新增或删除模型别名
- 调整别名对应的真实模型
- 配置 fallback 策略
### 服务配置
服务端口与环境变量位于 docker-compose.yml,可以根据部署环境调整:
- 端口映射
- 代理配置
- WebUI 认证开关
- 挂载目录和数据卷
## 注意事项
- 当前 WEBUI_AUTH=false,默认无登录认证,不建议直接暴露到公网
- .env 中包含真实密钥时,不应提交到公开仓库
- 如果代理地址不可用,LiteLLM 可能无法正常访问 Gemini 接口
- app/ 和 open_webui/ 目录当前未承载实际代码,主要运行逻辑集中在 docker-compose.yml 与 litellm/config.yaml
## 故障排查
### Open WebUI 无法加载模型
优先检查:
- LiteLLM 容器是否正常启动
- OPENAI_API_BASE_URL 是否仍然指向 <http://litellm:4000/v1>
- GEMINI_API_KEY 是否配置正确
### 模型调用失败
优先检查:
- 当前 API Key 是否有效
- 代理是否可用
- LiteLLM 日志中是否有上游模型接口错误
可使用以下命令查看日志:
```bash
docker compose logs -f litellm
docker compose logs -f open-webui
```
+33
View File
@@ -0,0 +1,33 @@
host: ''
port: 8317
remote-management:
allow-remote: false
secret-key: 'replace-with-a-strong-secret'
disable-control-panel: false
auth-dir: '/root/.cli-proxy-api'
api-keys:
- 'replace-with-an-api-key'
debug: false
commercial-mode: false
logging-to-file: false
usage-statistics-enabled: false
proxy-url: 'http://host.docker.internal:6152'
request-retry: 3
ws-auth: false
disable-image-generation: 'chat'
routing:
strategy: 'round-robin'
session-affinity: true
session-affinity-ttl: '1h'
oauth-model-alias:
codex:
- name: 'gpt-5.5'
alias: 'GPT 5.5'
fork: true
force-mapping: true
+118
View File
@@ -0,0 +1,118 @@
name: ai_chat
services:
cli-proxy-api:
image: eceasy/cli-proxy-api:latest
container_name: cli-proxy-api
pull_policy: always
ports:
- '127.0.0.1:8317:8317'
- '127.0.0.1:8085:8085'
- '127.0.0.1:1455:1455'
- '127.0.0.1:54545:54545'
- '127.0.0.1:51121:51121'
- '127.0.0.1:11451:11451'
environment:
- HTTP_PROXY=http://192.168.1.142:6152
- HTTPS_PROXY=http://192.168.1.142:6152
- ALL_PROXY=http://192.168.1.142:6152
- NO_PROXY=localhost,127.0.0.1,litellm,open-webui,cli-proxy-api
- no_proxy=localhost,127.0.0.1,litellm,open-webui,cli-proxy-api
volumes:
- ./cliproxyapi/config.yaml:/CLIProxyAPI/config.yaml:ro
- ./cliproxyapi/auths:/root/.cli-proxy-api
- ./cliproxyapi/logs:/CLIProxyAPI/logs
restart: unless-stopped
litellm:
image: docker.litellm.ai/berriai/litellm:main-stable
container_name: litellm
ports:
- '4000:4000'
environment:
- GEMINI_API_KEY=${GEMINI_API_KEY}
- HTTP_PROXY=http://192.168.1.142:6152
- HTTPS_PROXY=http://192.168.1.142:6152
- ALL_PROXY=http://192.168.1.142:6152
- NO_PROXY=localhost,127.0.0.1,litellm,open-webui,cli-proxy-api
- no_proxy=localhost,127.0.0.1,litellm,open-webui,cli-proxy-api
- GOOGLE_APPLICATION_CREDENTIALS=/app/vertex_key.json
- CLIPROXY_API_KEY=${CLIPROXY_API_KEY}
volumes:
- ./litellm:/app/litellm:ro
- ./litellm/gen-lang-client-0934968879-03bf3a88d4dd.json:/app/vertex_key.json
env_file:
- .env
depends_on:
- cli-proxy-api
command: >
--config /app/litellm/config.yaml
--port 4000
restart: unless-stopped
jupyter:
image: quay.io/jupyter/datascience-notebook:latest
container_name: ai-chat-jupyter
environment:
- JUPYTER_TOKEN=${JUPYTER_TOKEN}
command: >
start-notebook.py
--ServerApp.token=${JUPYTER_TOKEN}
--ServerApp.password=
--ServerApp.allow_origin=*
volumes:
- jupyter-work:/home/jovyan/work
restart: unless-stopped
open-webui:
image: ghcr.io/open-webui/open-webui:main
container_name: open-webui
pull_policy: always
ports:
- '3000:8080'
env_file:
- .env
environment:
- OPENAI_API_BASE_URLS=http://litellm:4000/v1;http://cli-proxy-api:8317/v1
- OPENAI_API_KEYS=dummy;${CLIPROXY_API_KEY}
- WEBUI_NAME=AI Chat
- WEBUI_AUTH=true
- ENABLE_SIGNUP=true
- ENABLE_LOGIN_FORM=true
- ENABLE_OAUTH_SIGNUP=true
- DEFAULT_USER_ROLE=pending
- OAUTH_ALLOWED_DOMAINS=${OAUTH_ALLOWED_DOMAINS}
- GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID}
- GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET}
- GOOGLE_REDIRECT_URI=${GOOGLE_REDIRECT_URI}
- GOOGLE_OAUTH_SCOPE=${GOOGLE_OAUTH_SCOPE}
- ENABLE_IMAGE_GENERATION=true
- ENABLE_IMAGE_PROMPT_GENERATION=true
- IMAGE_GENERATION_ENGINE=openai
- IMAGE_GENERATION_MODEL=imagen-4.0-generate-001-vertex
- IMAGE_SIZE=1024x1024
- IMAGES_OPENAI_API_BASE_URL=http://litellm:4000/v1
- IMAGES_OPENAI_API_KEY=dummy
- ENABLE_CODE_INTERPRETER=true
- CODE_INTERPRETER_ENGINE=jupyter
- CODE_INTERPRETER_JUPYTER_URL=http://jupyter:8888
- CODE_INTERPRETER_JUPYTER_AUTH=token
- CODE_INTERPRETER_JUPYTER_AUTH_TOKEN=${JUPYTER_TOKEN}
- CODE_INTERPRETER_JUPYTER_TIMEOUT=120
- ENABLE_CODE_EXECUTION=true
- CODE_EXECUTION_ENGINE=jupyter
- CODE_EXECUTION_JUPYTER_URL=http://jupyter:8888
- CODE_EXECUTION_JUPYTER_AUTH=token
- CODE_EXECUTION_JUPYTER_AUTH_TOKEN=${JUPYTER_TOKEN}
- CODE_EXECUTION_JUPYTER_TIMEOUT=120
volumes:
- open-webui:/app/backend/data
depends_on:
- litellm
- cli-proxy-api
- jupyter
restart: unless-stopped
volumes:
open-webui:
jupyter-work:
+429
View File
@@ -0,0 +1,429 @@
# AI Stack Windows service installer
# Run in elevated PowerShell:
# powershell -NoProfile -ExecutionPolicy Bypass -File .\install.ps1
param(
[switch]$Uninstall,
[switch]$Start,
[switch]$Stop,
[switch]$Status,
[string]$VenvDir,
[switch]$SkipVenvBootstrap
)
$ErrorActionPreference = "Stop"
$BASE_DIR = $PSScriptRoot
$WINSW_EXE = Join-Path $BASE_DIR "WinSW.exe"
$LITELLM_XML = Join-Path $BASE_DIR "litellm\litellm-service.xml"
$WEBUI_XML = Join-Path $BASE_DIR "open_webui\openwebui-service.xml"
$ENV_FILE = Join-Path $BASE_DIR ".env"
$LOG_DIR = Join-Path $BASE_DIR "logs\install"
$INSTALL_LOG = Join-Path $LOG_DIR ("install-{0}.log" -f (Get-Date -Format "yyyyMMdd-HHmmss"))
# Venv paths for each service (separate venv for independence)
$LITELLM_VENV = Join-Path $BASE_DIR "litellm\venv"
$WEBUI_VENV = Join-Path $BASE_DIR "open_webui\venv"
function Fail {
param([string]$Message)
Write-Host "[ERROR] $Message" -ForegroundColor Red
throw $Message
}
function Step {
param([string]$Message)
Write-Host "[STEP] $Message" -ForegroundColor Cyan
}
function Diag {
param([string]$Message)
Write-Host "[DIAG] $Message" -ForegroundColor DarkGray
}
function Ensure-InstallLogDir {
if (-not (Test-Path $LOG_DIR)) {
New-Item -ItemType Directory -Path $LOG_DIR -Force | Out-Null
}
}
function Get-DotEnvValue {
param([string]$Name)
if (-not (Test-Path $ENV_FILE)) {
return $null
}
foreach ($line in Get-Content $ENV_FILE) {
$trimmed = $line.Trim()
if ([string]::IsNullOrWhiteSpace($trimmed)) { continue }
if ($trimmed.StartsWith("#")) { continue }
if ($trimmed -like "$Name=*") {
return $trimmed.Substring($Name.Length + 1).Trim()
}
}
return $null
}
function Ensure-GeminiApiKey {
$apiKey = $env:GEMINI_API_KEY
if ([string]::IsNullOrWhiteSpace($apiKey)) {
$apiKey = Get-DotEnvValue -Name "GEMINI_API_KEY"
}
if ([string]::IsNullOrWhiteSpace($apiKey)) {
Fail "GEMINI_API_KEY not found. Set a system environment variable or add GEMINI_API_KEY=... to .env"
}
[Environment]::SetEnvironmentVariable("GEMINI_API_KEY", $apiKey, "Machine")
$env:GEMINI_API_KEY = $apiKey
Diag "GEMINI_API_KEY synced to machine environment for Windows services"
}
function Check-Admin {
$identity = [System.Security.Principal.WindowsIdentity]::GetCurrent()
$principal = New-Object System.Security.Principal.WindowsPrincipal($identity)
if (-not $principal.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator)) {
Fail "Please run PowerShell as Administrator."
}
}
function Test-PEExecutable {
param(
[string]$Path,
[string]$Name
)
if (-not (Test-Path $Path)) {
Fail "$Name not found: $Path"
}
$file = Get-Item $Path
$bytes = [System.IO.File]::ReadAllBytes($Path)
$header = if ($bytes.Length -ge 2) { "{0:X2} {1:X2}" -f $bytes[0], $bytes[1] } else { "N/A" }
Diag "$Name path: $Path"
Diag "$Name size: $($file.Length) bytes"
Diag "$Name header: $header"
if ($bytes.Length -lt 2 -or $bytes[0] -ne 0x4D -or $bytes[1] -ne 0x5A) {
Fail "$Name is not a valid Windows executable (missing MZ header)."
}
}
function Invoke-External {
param(
[Parameter(Mandatory = $true)]
[string]$File,
[string[]]$Arguments = @(),
[Parameter(Mandatory = $true)]
[string]$ActionLabel
)
$display = if ($Arguments.Count -gt 0) { " " + ($Arguments -join " ") } else { "" }
Step "$ActionLabel -> $File$display"
try {
& $File @Arguments
if ($LASTEXITCODE -ne $null -and $LASTEXITCODE -ne 0) {
Fail "$ActionLabel failed, exit code: $LASTEXITCODE"
}
} catch {
Write-Host "[ERROR] file: $File" -ForegroundColor Red
Write-Host "[ERROR] args: $($Arguments -join ' ')" -ForegroundColor Red
Write-Host "[ERROR] detail: $($_.Exception.Message)" -ForegroundColor Red
throw
}
}
function Check-WinSW {
if (-not (Test-Path $WINSW_EXE)) {
Fail "WinSW.exe not found in project root. Download WinSW-x64.exe and rename to WinSW.exe."
}
$file = Get-Item $WINSW_EXE
if ($file.Length -lt 100KB) {
Fail "WinSW.exe file size looks invalid ($($file.Length) bytes)."
}
Test-PEExecutable -Path $WINSW_EXE -Name "WinSW.exe"
Step "Validate WinSW executable"
Invoke-External -File $WINSW_EXE -Arguments @("--version") -ActionLabel "Check WinSW version"
}
function Resolve-VenvDir {
param([string]$VenvPath)
$pythonExe = Join-Path $VenvPath "Scripts\python.exe"
return (Test-Path $pythonExe)
}
function New-VenvIfNeeded {
param(
[Parameter(Mandatory = $true)]
[string]$VenvPath,
[Parameter(Mandatory = $true)]
[string]$ServiceName
)
$pythonExe = Join-Path $VenvPath "Scripts\python.exe"
if (Test-Path $pythonExe) {
return $VenvPath
}
if ($SkipVenvBootstrap) {
Fail "No virtual environment found at $VenvPath and -SkipVenvBootstrap is set."
}
Step "Create virtual environment for $ServiceName : $VenvPath"
$created = $false
try {
& py -3.11 -m venv $VenvPath
if ((Test-Path $pythonExe) -and $LASTEXITCODE -eq 0) { $created = $true }
} catch {}
if (-not $created) {
try {
& py -m venv $VenvPath
if ((Test-Path $pythonExe) -and $LASTEXITCODE -eq 0) { $created = $true }
} catch {}
}
if (-not $created) {
try {
& python -m venv $VenvPath
if ((Test-Path $pythonExe) -and $LASTEXITCODE -eq 0) { $created = $true }
} catch {}
}
if (-not $created) {
Fail "Failed to create virtual environment at $VenvPath. Ensure Python 3.11 is installed."
}
Write-Host "[OK] virtual environment created: $VenvPath" -ForegroundColor Green
return $VenvPath
}
function Install-LiteLLMDependencies {
param([string]$VenvPath)
$venvPython = Join-Path $VenvPath "Scripts\python.exe"
$litellmExe = Join-Path $VenvPath "Scripts\litellm.exe"
if (Test-Path $litellmExe) {
Diag "LiteLLM already installed in $VenvPath"
return
}
if ($SkipVenvBootstrap) {
Fail "Missing litellm in venv and -SkipVenvBootstrap is set."
}
Step "Install LiteLLM dependencies"
Invoke-External -File $venvPython -Arguments @("-m", "pip", "install", "--upgrade", "pip", "setuptools", "wheel") -ActionLabel "Upgrade pip toolchain (LiteLLM)"
# Install litellm[proxy] with email-validator for pydantic
Invoke-External -File $venvPython -Arguments @("-m", "pip", "install", "email-validator") -ActionLabel "Install email-validator"
Invoke-External -File $venvPython -Arguments @("-m", "pip", "install", "litellm[proxy]") -ActionLabel "Install litellm[proxy]"
if (-not (Test-Path $litellmExe)) {
Fail "LiteLLM installed but litellm.exe not found at $litellmExe"
}
}
function Install-OpenWebUIDependencies {
param([string]$VenvPath)
$venvPython = Join-Path $VenvPath "Scripts\python.exe"
$webuiExe = Join-Path $VenvPath "Scripts\open-webui.exe"
if (Test-Path $webuiExe) {
Diag "Open WebUI already installed in $VenvPath"
return
}
if ($SkipVenvBootstrap) {
Fail "Missing open-webui in venv and -SkipVenvBootstrap is set."
}
Step "Install Open WebUI dependencies"
Invoke-External -File $venvPython -Arguments @("-m", "pip", "install", "--upgrade", "pip", "setuptools", "wheel") -ActionLabel "Upgrade pip toolchain (Open WebUI)"
Invoke-External -File $venvPython -Arguments @("-m", "pip", "install", "open-webui") -ActionLabel "Install open-webui"
if (-not (Test-Path $webuiExe)) {
Fail "Open WebUI installed but open-webui.exe not found at $webuiExe"
}
}
function Check-Venvs {
Step "Check/create LiteLLM venv"
$script:LiteLLMVenv = New-VenvIfNeeded -VenvPath $LITELLM_VENV -ServiceName "LiteLLM"
Install-LiteLLMDependencies -VenvPath $script:LiteLLMVenv
Step "Check/create Open WebUI venv"
$script:WebUIVenv = New-VenvIfNeeded -VenvPath $WEBUI_VENV -ServiceName "Open WebUI"
Install-OpenWebUIDependencies -VenvPath $script:WebUIVenv
$litellmExe = Join-Path $script:LiteLLMVenv "Scripts\litellm.exe"
$webuiExe = Join-Path $script:WebUIVenv "Scripts\open-webui.exe"
if (-not (Test-Path $litellmExe)) { Fail "litellm.exe not found: $litellmExe" }
if (-not (Test-Path $webuiExe)) { Fail "open-webui.exe not found: $webuiExe" }
Diag "LiteLLM venv: $script:LiteLLMVenv"
Diag "Open WebUI venv: $script:WebUIVenv"
}
function Ensure-DataDirs {
$dirs = @(
(Join-Path $BASE_DIR "logs\litellm"),
(Join-Path $BASE_DIR "logs\openwebui"),
(Join-Path $BASE_DIR ".data\open-webui")
)
foreach ($d in $dirs) {
if (-not (Test-Path $d)) {
New-Item -ItemType Directory -Path $d -Force | Out-Null
Diag "Created directory: $d"
}
}
}
function Install-Services {
Write-Host ""
Write-Host "=== Install AI Stack Windows Services ===" -ForegroundColor Cyan
Write-Host ""
Write-Host "This install preserves existing config and data directories." -ForegroundColor Yellow
Write-Host "It only creates missing venvs, installs packages, and registers services." -ForegroundColor Yellow
Write-Host ""
Check-Admin
Check-WinSW
Ensure-GeminiApiKey
Check-Venvs
Ensure-DataDirs
$litellmWinSW = Join-Path $BASE_DIR "litellm\WinSW.exe"
$webuiWinSW = Join-Path $BASE_DIR "open_webui\WinSW.exe"
Step "Copy WinSW.exe into service folders"
Copy-Item $WINSW_EXE $litellmWinSW -Force
Copy-Item $WINSW_EXE $webuiWinSW -Force
Test-PEExecutable -Path $litellmWinSW -Name "litellm\\WinSW.exe"
Test-PEExecutable -Path $webuiWinSW -Name "open_webui\\WinSW.exe"
Invoke-External -File $litellmWinSW -Arguments @("--version") -ActionLabel "Validate litellm WinSW"
Invoke-External -File $webuiWinSW -Arguments @("--version") -ActionLabel "Validate open_webui WinSW"
Invoke-External -File $litellmWinSW -Arguments @("install", $LITELLM_XML) -ActionLabel "Install LiteLLM service"
Invoke-External -File $webuiWinSW -Arguments @("install", $WEBUI_XML) -ActionLabel "Install Open WebUI service"
Step "Start services"
Start-Service ai-stack-litellm
Start-Sleep -Seconds 3
Start-Service ai-stack-openwebui
Write-Host ""
Write-Host "[OK] Installation completed" -ForegroundColor Green
Write-Host "Open WebUI: http://127.0.0.1:3000"
Write-Host "LiteLLM API: http://127.0.0.1:4000"
Write-Host "Configuration and persistent data were preserved." -ForegroundColor Green
Write-Host ""
}
function Uninstall-Services {
Write-Host ""
Write-Host "=== Uninstall AI Stack Windows Services ===" -ForegroundColor Yellow
Write-Host ""
Check-Admin
$litellmWinSW = Join-Path $BASE_DIR "litellm\WinSW.exe"
$webuiWinSW = Join-Path $BASE_DIR "open_webui\WinSW.exe"
foreach ($svc in @("ai-stack-openwebui", "ai-stack-litellm")) {
$s = Get-Service -Name $svc -ErrorAction SilentlyContinue
if ($s -and $s.Status -eq "Running") {
Step "Stop service $svc"
Stop-Service $svc -Force
}
}
if (Test-Path $webuiWinSW) {
Invoke-External -File $webuiWinSW -Arguments @("uninstall", $WEBUI_XML) -ActionLabel "Uninstall Open WebUI service"
}
if (Test-Path $litellmWinSW) {
Invoke-External -File $litellmWinSW -Arguments @("uninstall", $LITELLM_XML) -ActionLabel "Uninstall LiteLLM service"
}
Write-Host ""
Write-Host "[OK] Uninstall completed" -ForegroundColor Green
Write-Host ""
}
function Start-Services {
Check-Admin
Step "Start LiteLLM"
Start-Service ai-stack-litellm
Start-Sleep -Seconds 3
Step "Start Open WebUI"
Start-Service ai-stack-openwebui
}
function Stop-Services {
Check-Admin
Step "Stop Open WebUI"
Stop-Service ai-stack-openwebui -Force -ErrorAction SilentlyContinue
Step "Stop LiteLLM"
Stop-Service ai-stack-litellm -Force -ErrorAction SilentlyContinue
}
function Show-Status {
Write-Host ""
Write-Host "=== AI Stack Service Status ===" -ForegroundColor Cyan
Write-Host ""
foreach ($svc in @("ai-stack-litellm", "ai-stack-openwebui")) {
$s = Get-Service -Name $svc -ErrorAction SilentlyContinue
if ($s) {
$color = if ($s.Status -eq "Running") { "Green" } else { "Red" }
Write-Host " $($s.DisplayName): $($s.Status)" -ForegroundColor $color
} else {
Write-Host " ${svc}: not installed" -ForegroundColor DarkGray
}
}
Write-Host ""
}
Ensure-InstallLogDir
Write-Host "Install log file: $INSTALL_LOG" -ForegroundColor DarkGray
Start-Transcript -Path $INSTALL_LOG -Append | Out-Null
try {
if ($Uninstall) {
Uninstall-Services
} elseif ($Start) {
Start-Services
} elseif ($Stop) {
Stop-Services
} elseif ($Status) {
Show-Status
} else {
Install-Services
}
} catch {
Write-Host ""
Write-Host "Install failed. Send this log file:" -ForegroundColor Red
Write-Host " $INSTALL_LOG" -ForegroundColor Red
Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red
throw
} finally {
Stop-Transcript | Out-Null
}
+56
View File
@@ -0,0 +1,56 @@
model_list:
- model_name: gemini-flash-latest
litellm_params:
model: vertex_ai/gemini-flash-latest
api_key: os.environ/GEMINI_API_KEY
- model_name: gemini-flash-lite-latest
litellm_params:
model: vertex_ai/gemini-flash-lite-latest
api_key: os.environ/GEMINI_API_KEY
- model_name: gemini-pro-latest
litellm_params:
model: vertex_ai/gemini-pro-latest
api_key: os.environ/GEMINI_API_KEY
- model_name: chatgpt-pro
litellm_params:
model: openai/chatgpt-pro
api_base: http://cli-proxy-api:8317/v1
api_key: os.environ/CLIPROXY_API_KEY
# # 供 Open WebUI 的图片生成功能调用。不要把这些模型当普通聊天模型直接在对话里选择,
# # 否则走 /chat/completions 流式返回大块 base64 时,前端可能报 Chunk too big。
# - model_name: gemini-3-pro-image-preview
# litellm_params:
# model: gemini/gemini-3-pro-image-preview
# api_key: os.environ/GEMINI_API_KEY
litellm_settings:
drop_params: true
router_settings:
num_retries: 3
timeout: 60
fallbacks:
- {
'gemini-flash-lite-latest':
['gemini-flash-latest', 'gemini-pro-latest'],
}
- { 'gemini-flash-latest': ['gemini-pro-latest'] }
#
# - model_name: gemini-flash-enterprise
# litellm_params:
# model: vertex_ai/gemini-2.5-flash
# vertex_project: 'gen-lang-client-0934968879'
# vertex_location: 'asia-southeast1'
# # vertex_credentials: '/app/litellm/gen-lang-client-0934968879-03bf3a88d4dd.json' # 容器内路径
# - model_name: gemini-pro-enterprise
# litellm_params:
# model: vertex_ai/gemini-2.5-pro
# vertex_project: 'gen-lang-client-0934968879'
# vertex_location: 'asia-southeast1'
# vertex_credentials: '/app/litellm/gen-lang-client-0934968879-03bf3a88d4dd.json' # 容器内路径
+45
View File
@@ -0,0 +1,45 @@
<service>
<id>ai-stack-litellm</id>
<name>AI Stack - LiteLLM</name>
<description>LiteLLM proxy for Gemini models, provides OpenAI-compatible API on port 4000</description>
<!--
使用 WinSW 的 %BASE% 相对路径。
%BASE% = WinSW.exe 所在目录(本文件所在目录:...\litellm)。
现在使用专属 venv..\litellm\venv,支持任意盘符部署。
-->
<executable>%BASE%\venv\Scripts\litellm.exe</executable>
<arguments>--config %BASE%\config.yaml --port 4000</arguments>
<workingdirectory>%BASE%\..</workingdirectory>
<!-- Gemini API Key 从 Windows 系统环境变量读取;install.ps1/update.ps1 会从 .env 同步 -->
<env name="GEMINI_API_KEY" value="%GEMINI_API_KEY%"/>
<!--
如果访问 Gemini 需要代理,取消注释并修改代理地址
-->
<!-- <env name="HTTP_PROXY" value="http://127.0.0.1:7890"/> -->
<!-- <env name="HTTPS_PROXY" value="http://127.0.0.1:7890"/> -->
<!-- <env name="ALL_PROXY" value="http://127.0.0.1:7890"/> -->
<!-- 日志,滚动模式防止无限增长 -->
<logpath>%BASE%\..\logs\litellm</logpath>
<log mode="roll">
<sizeThreshold>10240</sizeThreshold>
<keepFiles>5</keepFiles>
</log>
<!-- 开机自动启动,延迟启动避免网络未就绪 -->
<startmode>Automatic</startmode>
<delayedAutoStart>true</delayedAutoStart>
<!-- 失败自动重启,10 秒后重试,连续失败也持续重启 -->
<onfailure action="restart" delay="10 sec"/>
<resetfailure>1 hour</resetfailure>
<!-- 停止等待时间 -->
<stoptimeout>15 sec</stoptimeout>
<!-- 不显示控制台窗口 -->
<hidewindow>true</hidewindow>
</service>
+56
View File
@@ -0,0 +1,56 @@
<service>
<id>ai-stack-openwebui</id>
<name>AI Stack - Open WebUI</name>
<description>Open WebUI chat interface connected to LiteLLM, runs on port 3000</description>
<!--
使用 WinSW 的 %BASE% 相对路径。
%BASE% = WinSW.exe 所在目录(本文件所在目录:...\open_webui)。
现在使用专属 venv..\open_webui\venv,支持任意盘符部署。
-->
<executable>%BASE%\venv\Scripts\open-webui.exe</executable>
<arguments>serve --port 3000</arguments>
<workingdirectory>%BASE%\..</workingdirectory>
<!-- 指向本地 LiteLLM -->
<env name="OPENAI_API_BASE_URL" value="http://127.0.0.1:4000/v1"/>
<env name="OPENAI_API_KEY" value="dummy"/>
<!-- 关闭登录认证,内网/测试环境使用;生产环境建议改为 true -->
<env name="WEBUI_AUTH" value="false"/>
<!-- 数据持久化目录 -->
<env name="DATA_DIR" value="%BASE%\..\.data\open-webui"/>
<!-- 禁用运行时 pip 安装,加快启动速度 -->
<env name="ENABLE_PIP_INSTALL_FRONTMATTER_REQUIREMENTS" value="False"/>
<!-- 禁止模型自动更新(避免启动时联网下载) -->
<env name="RAG_EMBEDDING_MODEL_AUTO_UPDATE" value="false"/>
<env name="RAG_RERANKING_MODEL_AUTO_UPDATE" value="false"/>
<env name="WHISPER_MODEL_AUTO_UPDATE" value="false"/>
<!-- 日志,滚动模式防止无限增长 -->
<logpath>%BASE%\..\logs\openwebui</logpath>
<log mode="roll">
<sizeThreshold>10240</sizeThreshold>
<keepFiles>5</keepFiles>
</log>
<!-- 开机自动启动,延迟启动确保 LiteLLM 先就绪 -->
<startmode>Automatic</startmode>
<delayedAutoStart>true</delayedAutoStart>
<!-- 失败自动重启 -->
<onfailure action="restart" delay="10 sec"/>
<resetfailure>1 hour</resetfailure>
<!-- 停止等待时间 -->
<stoptimeout>15 sec</stoptimeout>
<!-- 不显示控制台窗口 -->
<hidewindow>true</hidewindow>
<!-- 依赖 LiteLLM 服务,确保 LiteLLM 先启动 -->
<depend>ai-stack-litellm</depend>
</service>
+153
View File
@@ -0,0 +1,153 @@
# AI Stack Windows service uninstaller
# Run in elevated PowerShell:
# powershell -NoProfile -ExecutionPolicy Bypass -File .\uninstall.ps1
param(
[switch]$RemoveVenv,
[switch]$RemoveData
)
$ErrorActionPreference = "Stop"
$BASE_DIR = $PSScriptRoot
$LITELLM_XML = Join-Path $BASE_DIR "litellm\litellm-service.xml"
$WEBUI_XML = Join-Path $BASE_DIR "open_webui\openwebui-service.xml"
$LITELLM_VENV = Join-Path $BASE_DIR "litellm\venv"
$WEBUI_VENV = Join-Path $BASE_DIR "open_webui\venv"
$DATA_DIR = Join-Path $BASE_DIR ".data\open-webui"
function Fail {
param([string]$Message)
Write-Host "[ERROR] $Message" -ForegroundColor Red
throw $Message
}
function Step {
param([string]$Message)
Write-Host "[STEP] $Message" -ForegroundColor Cyan
}
function Diag {
param([string]$Message)
Write-Host "[DIAG] $Message" -ForegroundColor DarkGray
}
function Check-Admin {
$identity = [System.Security.Principal.WindowsIdentity]::GetCurrent()
$principal = New-Object System.Security.Principal.WindowsPrincipal($identity)
if (-not $principal.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator)) {
Fail "Please run PowerShell as Administrator."
}
}
function Invoke-External {
param(
[Parameter(Mandatory = $true)]
[string]$File,
[string[]]$Arguments = @(),
[Parameter(Mandatory = $true)]
[string]$ActionLabel
)
$display = if ($Arguments.Count -gt 0) { " " + ($Arguments -join " ") } else { "" }
Step "$ActionLabel -> $File$display"
try {
& $File @Arguments
if ($LASTEXITCODE -ne $null -and $LASTEXITCODE -ne 0) {
Fail "$ActionLabel failed, exit code: $LASTEXITCODE"
}
} catch {
Write-Host "[ERROR] file: $File" -ForegroundColor Red
Write-Host "[ERROR] args: $($Arguments -join ' ')" -ForegroundColor Red
Write-Host "[ERROR] detail: $($_.Exception.Message)" -ForegroundColor Red
throw
}
}
function Uninstall-All {
Write-Host ""
Write-Host "=== Uninstall AI Stack Windows Services ===" -ForegroundColor Yellow
Write-Host ""
Check-Admin
$litellmWinSW = Join-Path $BASE_DIR "litellm\WinSW.exe"
$webuiWinSW = Join-Path $BASE_DIR "open_webui\WinSW.exe"
# Stop services in reverse order (Open WebUI depends on LiteLLM)
Step "Stop Open WebUI service"
$webuiSvc = Get-Service -Name "ai-stack-openwebui" -ErrorAction SilentlyContinue
if ($webuiSvc -and $webuiSvc.Status -eq "Running") {
Stop-Service ai-stack-openwebui -Force
Start-Sleep -Seconds 2
}
Step "Stop LiteLLM service"
$litellmSvc = Get-Service -Name "ai-stack-litellm" -ErrorAction SilentlyContinue
if ($litellmSvc -and $litellmSvc.Status -eq "Running") {
Stop-Service ai-stack-litellm -Force
Start-Sleep -Seconds 2
}
# Uninstall services
if (Test-Path $webuiWinSW) {
Invoke-External -File $webuiWinSW -Arguments @("uninstall", $WEBUI_XML) -ActionLabel "Uninstall Open WebUI service"
Start-Sleep -Seconds 1
}
if (Test-Path $litellmWinSW) {
Invoke-External -File $litellmWinSW -Arguments @("uninstall", $LITELLM_XML) -ActionLabel "Uninstall LiteLLM service"
Start-Sleep -Seconds 1
}
# Optional: remove venv
if ($RemoveVenv) {
Step "Remove virtual environments"
if (Test-Path $LITELLM_VENV) {
Remove-Item $LITELLM_VENV -Recurse -Force
Diag "Removed: $LITELLM_VENV"
}
if (Test-Path $WEBUI_VENV) {
Remove-Item $WEBUI_VENV -Recurse -Force
Diag "Removed: $WEBUI_VENV"
}
} else {
Diag "Keeping virtual environments by default (use -RemoveVenv to delete them)"
}
# Optional: remove data
if ($RemoveData) {
Step "Remove Open WebUI data"
if (Test-Path $DATA_DIR) {
Remove-Item $DATA_DIR -Recurse -Force
Diag "Removed: $DATA_DIR"
}
} else {
Diag "Keeping Open WebUI data by default (use -RemoveData to delete it)"
}
# Remove WinSW copies from service folders
Step "Clean up WinSW from service folders"
Remove-Item $litellmWinSW -Force -ErrorAction SilentlyContinue
Remove-Item $webuiWinSW -Force -ErrorAction SilentlyContinue
Write-Host ""
Write-Host "[OK] Uninstall completed" -ForegroundColor Green
Write-Host ""
Write-Host "Usage for next install:" -ForegroundColor Cyan
Write-Host " powershell -NoProfile -ExecutionPolicy Bypass -File .\install.ps1"
Write-Host ""
Write-Host "To remove venv/data too:" -ForegroundColor Cyan
Write-Host " powershell -NoProfile -ExecutionPolicy Bypass -File .\uninstall.ps1 -RemoveVenv -RemoveData"
Write-Host ""
}
try {
Uninstall-All
} catch {
Write-Host ""
Write-Host "Uninstall failed" -ForegroundColor Red
Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red
throw
}
+224
View File
@@ -0,0 +1,224 @@
# AI Stack Windows service updater
# Run in elevated PowerShell:
# powershell -NoProfile -ExecutionPolicy Bypass -File .\update.ps1
param(
[switch]$SkipRestart
)
$ErrorActionPreference = "Stop"
$BASE_DIR = $PSScriptRoot
$WINSW_EXE = Join-Path $BASE_DIR "WinSW.exe"
$LITELLM_XML = Join-Path $BASE_DIR "litellm\litellm-service.xml"
$WEBUI_XML = Join-Path $BASE_DIR "open_webui\openwebui-service.xml"
$ENV_FILE = Join-Path $BASE_DIR ".env"
$LITELLM_VENV = Join-Path $BASE_DIR "litellm\venv"
$WEBUI_VENV = Join-Path $BASE_DIR "open_webui\venv"
$LOG_DIR = Join-Path $BASE_DIR "logs\install"
$UPDATE_LOG = Join-Path $LOG_DIR ("update-{0}.log" -f (Get-Date -Format "yyyyMMdd-HHmmss"))
function Fail {
param([string]$Message)
Write-Host "[ERROR] $Message" -ForegroundColor Red
throw $Message
}
function Step {
param([string]$Message)
Write-Host "[STEP] $Message" -ForegroundColor Cyan
}
function Diag {
param([string]$Message)
Write-Host "[DIAG] $Message" -ForegroundColor DarkGray
}
function Ensure-LogDir {
if (-not (Test-Path $LOG_DIR)) {
New-Item -ItemType Directory -Path $LOG_DIR -Force | Out-Null
}
}
function Get-DotEnvValue {
param([string]$Name)
if (-not (Test-Path $ENV_FILE)) {
return $null
}
foreach ($line in Get-Content $ENV_FILE) {
$trimmed = $line.Trim()
if ([string]::IsNullOrWhiteSpace($trimmed)) { continue }
if ($trimmed.StartsWith("#")) { continue }
if ($trimmed -like "$Name=*") {
return $trimmed.Substring($Name.Length + 1).Trim()
}
}
return $null
}
function Ensure-GeminiApiKey {
$apiKey = $env:GEMINI_API_KEY
if ([string]::IsNullOrWhiteSpace($apiKey)) {
$apiKey = Get-DotEnvValue -Name "GEMINI_API_KEY"
}
if ([string]::IsNullOrWhiteSpace($apiKey)) {
Fail "GEMINI_API_KEY not found. Set a system environment variable or add GEMINI_API_KEY=... to .env"
}
[Environment]::SetEnvironmentVariable("GEMINI_API_KEY", $apiKey, "Machine")
$env:GEMINI_API_KEY = $apiKey
Diag "GEMINI_API_KEY synced to machine environment for Windows services"
}
function Check-Admin {
$identity = [System.Security.Principal.WindowsIdentity]::GetCurrent()
$principal = New-Object System.Security.Principal.WindowsPrincipal($identity)
if (-not $principal.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator)) {
Fail "Please run PowerShell as Administrator."
}
}
function Invoke-External {
param(
[Parameter(Mandatory = $true)]
[string]$File,
[string[]]$Arguments = @(),
[Parameter(Mandatory = $true)]
[string]$ActionLabel
)
$display = if ($Arguments.Count -gt 0) { " " + ($Arguments -join " ") } else { "" }
Step "$ActionLabel -> $File$display"
try {
& $File @Arguments
if ($LASTEXITCODE -ne $null -and $LASTEXITCODE -ne 0) {
Fail "$ActionLabel failed, exit code: $LASTEXITCODE"
}
} catch {
Write-Host "[ERROR] file: $File" -ForegroundColor Red
Write-Host "[ERROR] args: $($Arguments -join ' ')" -ForegroundColor Red
Write-Host "[ERROR] detail: $($_.Exception.Message)" -ForegroundColor Red
throw
}
}
function Check-Prerequisites {
if (-not (Test-Path $WINSW_EXE)) {
Fail "WinSW.exe not found in project root. Download WinSW-x64.exe and rename to WinSW.exe."
}
foreach ($path in @($LITELLM_XML, $WEBUI_XML)) {
if (-not (Test-Path $path)) {
Fail "Required file not found: $path"
}
}
foreach ($venvPath in @($LITELLM_VENV, $WEBUI_VENV)) {
$pythonExe = Join-Path $venvPath "Scripts\python.exe"
if (-not (Test-Path $pythonExe)) {
Fail "Virtual environment not found: $venvPath. Run install.ps1 first."
}
}
}
function Stop-ServicesIfPresent {
foreach ($svc in @("ai-stack-openwebui", "ai-stack-litellm")) {
$service = Get-Service -Name $svc -ErrorAction SilentlyContinue
if ($service -and $service.Status -eq "Running") {
Step "Stop service $svc"
Stop-Service $svc -Force
Start-Sleep -Seconds 2
}
}
}
function Upgrade-VenvPackages {
param(
[string]$PythonExe,
[string[]]$Packages,
[string]$Name
)
Invoke-External -File $PythonExe -Arguments @("-m", "pip", "install", "--upgrade", "pip", "setuptools", "wheel") -ActionLabel "Upgrade pip toolchain ($Name)"
$upgradeArgs = @("-m", "pip", "install", "--upgrade") + $Packages
Invoke-External -File $PythonExe -Arguments $upgradeArgs -ActionLabel "Upgrade packages ($Name)"
}
function Refresh-ServiceBinaries {
$litellmWinSW = Join-Path $BASE_DIR "litellm\WinSW.exe"
$webuiWinSW = Join-Path $BASE_DIR "open_webui\WinSW.exe"
Step "Sync WinSW.exe into service folders"
Copy-Item $WINSW_EXE $litellmWinSW -Force
Copy-Item $WINSW_EXE $webuiWinSW -Force
}
function Start-ServicesIfNeeded {
if ($SkipRestart) {
Diag "SkipRestart is set, services remain stopped after package upgrade."
return
}
Step "Start LiteLLM service"
Start-Service ai-stack-litellm
Start-Sleep -Seconds 3
Step "Start Open WebUI service"
Start-Service ai-stack-openwebui
}
function Update-All {
Write-Host ""
Write-Host "=== Update AI Stack Windows Services ===" -ForegroundColor Cyan
Write-Host ""
Write-Host "This update preserves config files, Open WebUI data, and existing venv directories." -ForegroundColor Yellow
Write-Host "It only upgrades packages and refreshes service runtime files." -ForegroundColor Yellow
Write-Host ""
Check-Admin
Check-Prerequisites
Ensure-GeminiApiKey
Stop-ServicesIfPresent
Refresh-ServiceBinaries
$litellmPython = Join-Path $LITELLM_VENV "Scripts\python.exe"
$webuiPython = Join-Path $WEBUI_VENV "Scripts\python.exe"
Upgrade-VenvPackages -PythonExe $litellmPython -Packages @("email-validator", "litellm[proxy]") -Name "LiteLLM"
Upgrade-VenvPackages -PythonExe $webuiPython -Packages @("open-webui") -Name "Open WebUI"
Start-ServicesIfNeeded
Write-Host ""
Write-Host "[OK] Update completed" -ForegroundColor Green
Write-Host "Configuration and data were preserved." -ForegroundColor Green
if ($SkipRestart) {
Write-Host "Services were not restarted because -SkipRestart was specified." -ForegroundColor Yellow
} else {
Write-Host "Open WebUI: http://127.0.0.1:3000"
Write-Host "LiteLLM API: http://127.0.0.1:4000"
}
Write-Host ""
}
Ensure-LogDir
Write-Host "Update log file: $UPDATE_LOG" -ForegroundColor DarkGray
Start-Transcript -Path $UPDATE_LOG -Append | Out-Null
try {
Update-All
} catch {
Write-Host ""
Write-Host "Update failed. Send this log file:" -ForegroundColor Red
Write-Host " $UPDATE_LOG" -ForegroundColor Red
Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red
throw
} finally {
Stop-Transcript | Out-Null
}