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