Initial commit
This commit is contained in:
@@ -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
|
||||
@@ -0,0 +1,6 @@
|
||||
.DS_Store
|
||||
.env
|
||||
cliproxyapi/config.yaml
|
||||
cliproxyapi/auths/
|
||||
cliproxyapi/logs/
|
||||
litellm/gen-lang-client-*.json
|
||||
@@ -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.yaml:LiteLLM 模型映射与回退策略配置
|
||||
- .env:本地环境变量文件,至少需要配置 Gemini API Key
|
||||
- app/:预留目录,当前未使用
|
||||
- open_webui/:Open WebUI 的 Windows 服务配置目录
|
||||
- install.ps1:Windows 服务安装/卸载/启停脚本(WinSW)
|
||||
- litellm/litellm-service.xml:LiteLLM 的 WinSW 服务配置
|
||||
- open_webui/openwebui-service.xml:Open 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
|
||||
```
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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' # 容器内路径
|
||||
@@ -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>
|
||||
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user