diff --git a/.claude/skills b/.claude/skills deleted file mode 120000 index cd2ebc5..0000000 --- a/.claude/skills +++ /dev/null @@ -1 +0,0 @@ -../doc/skills \ No newline at end of file diff --git a/.gitignore b/.gitignore index 0757bd3..428bc35 100644 --- a/.gitignore +++ b/.gitignore @@ -122,6 +122,14 @@ celerybeat.pid # Environments .env +docker_litellm/compose/artifacts/ +docker_openclaw/p6/artifacts/ +docker_openclaw/p6/p6-inputs.json +docker_openclaw/p6/.p6-work/ +docker_hermes/p7/artifacts/ +docker_hermes/p7/.p7-work/ +docker_hermes/p7/p7-inputs.json +docker_hermes/p7/source/ .venv env/ venv/ diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index e69de29..0000000 diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 120000 index 47dc3e3..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1 +0,0 @@ -AGENTS.md \ No newline at end of file diff --git a/docker_casdoor/demo/docker-compose.yml b/docker_casdoor/compose/docker-compose.yml similarity index 100% rename from docker_casdoor/demo/docker-compose.yml rename to docker_casdoor/compose/docker-compose.yml diff --git a/docker_clash/demo/docker-compose.yml b/docker_clash/compose/docker-compose.yml similarity index 100% rename from docker_clash/demo/docker-compose.yml rename to docker_clash/compose/docker-compose.yml diff --git a/docker_hermes/README.md b/docker_hermes/README.md index f3c06ac..3447032 100644 --- a/docker_hermes/README.md +++ b/docker_hermes/README.md @@ -2,6 +2,10 @@ `hermes` is a containerized agentic assistant platform based on the [Hermes Agent](https://github.com/nousresearch/hermes-agent) project, built using Node.js and Python runtime stacks. +Dockerfile 以固定的 Hermes repository 与 40 位 commit 作为制品输入,不以移动的 +`main` 构建。默认 standalone Compose 服务于本地开发,只接受本机已存在的明确镜像 +引用,不会静默 pull `latest`。 + --- ## 1. Port Configuration @@ -48,21 +52,42 @@ source ./tool.sh build_image_no_tag hermes local docker_hermes/hermes.Dockerfile ``` +### 可复现构建(固定源码提交) + +需要可复现构建时,使用固定 tag 与明确的 Hermes source identity: + +```bash +build_image_no_tag hermes src-<12hex> docker_hermes/hermes.Dockerfile \ + --build-arg HERMES_SOURCE_REPOSITORY= \ + --build-arg HERMES_SOURCE_COMMIT=<40-hex-commit> +``` + +镜像会记录 `org.opencontainers.image.source` 与 +`org.opencontainers.image.revision`,并在 `/opt/hermes/.labnow-source-*` 保存 +相同的非敏感 provenance。只在本地命名为 `quay.io/labnow/hermes:src-<12hex>`,不 push。 + +### Dashboard Chat TUI runtime + +Hermes 的 Dashboard 在 `/api/pty` 中执行已经构建的 +`/opt/hermes/ui-tui/dist/entry.js`。运行基础镜像不是 Node 镜像,因此 Dockerfile 会从 +同一目标架构的 builder 复制固定的 `/opt/node` runtime,并将其放入 `PATH`。这避免用户 +第一次打开 Chat 时触发 Node 下载/解压;不改变 Hermes source、TUI build 或模型配置。 + ### Start with Docker Compose 1. Copy the sample environment file: ```bash - cp docker_hermes/.env.example docker_hermes/demo/.env + cp docker_hermes/compose/.env.example docker_hermes/compose/.env ``` -2. Specify the built image in `docker_hermes/demo/.env`: +2. Specify the built image in `docker_hermes/compose/.env`: ```env HERMES_IMAGE=quay.io/labnow/hermes:local ``` 3. Launch the container: ```bash - docker compose --env-file docker_hermes/demo/.env -f docker_hermes/demo/docker-compose.yml up -d + docker compose --env-file docker_hermes/compose/.env -f docker_hermes/compose/docker-compose.yml up -d ``` ### Execution Modes @@ -89,7 +114,7 @@ python -c "from plugins.dashboard_auth.basic import hash_password; print(hash_pa ### Model Provider Setup -Hermes requires an LLM inference provider. Configure credentials in `docker_hermes/demo/.env`: +Hermes requires an LLM inference provider. Configure credentials in `docker_hermes/compose/.env`: ```env OPENAI_API_KEY=your-key diff --git a/docker_hermes/demo/.env.example b/docker_hermes/compose/.env.example similarity index 83% rename from docker_hermes/demo/.env.example rename to docker_hermes/compose/.env.example index 01eeabc..5beb2d8 100644 --- a/docker_hermes/demo/.env.example +++ b/docker_hermes/compose/.env.example @@ -1,13 +1,13 @@ # Hermes local runtime configuration. # -# Copy this file to docker_hermes/demo/.env or pass it with: -# docker compose --env-file docker_hermes/demo/.env.example -f docker_hermes/demo/docker-compose.yml up -d +# Copy this file to docker_hermes/compose/.env or pass it with: +# docker compose --env-file docker_hermes/compose/.env.example -f docker_hermes/compose/docker-compose.yml up -d TZ=Asia/Shanghai # Image to run. Build with REGISTRY_DST=quay.io via tool.sh before starting Compose. # Default local output: quay.io/labnow/hermes:local (no automatic push). -HERMES_IMAGE=quay.io/labnow/hermes +HERMES_IMAGE=quay.io/labnow/hermes:local # Host-side persistent data directory. HERMES_DATA_DIR=../../.data/hermes diff --git a/docker_hermes/demo/docker-compose.yml b/docker_hermes/compose/docker-compose.yml similarity index 87% rename from docker_hermes/demo/docker-compose.yml rename to docker_hermes/compose/docker-compose.yml index d04c4d9..cc4302b 100644 --- a/docker_hermes/demo/docker-compose.yml +++ b/docker_hermes/compose/docker-compose.yml @@ -4,8 +4,10 @@ services: hermes: container_name: ${HERMES_CONTAINER_NAME:-svc-hermes} hostname: svc-hermes - image: "${HERMES_IMAGE:-quay.io/labnow/hermes:latest}" - pull_policy: always + image: "${HERMES_IMAGE:?set a fixed local Hermes image}" + # Local development must not silently pull a mutable image. Use an + # explicit Compose action when an operator intentionally needs a pull. + pull_policy: never restart: unless-stopped env_file: [".env.example"] environment: diff --git a/docker_hermes/hermes.Dockerfile b/docker_hermes/hermes.Dockerfile index 1916e81..62ea5be 100644 --- a/docker_hermes/hermes.Dockerfile +++ b/docker_hermes/hermes.Dockerfile @@ -3,9 +3,19 @@ ARG BASE_NAMESPACE ARG BASE_IMG_BUILD="node" ARG BASE_IMG="base" +ARG HERMES_BUILD_BASE_IMAGE +ARG HERMES_RUNTIME_BASE_IMAGE +# The upstream source is a release input, not a moving branch. Keep the +# repository and commit overridable only so the local runner can bind both to +# its protected input and record the exact provenance. +ARG HERMES_SOURCE_REPOSITORY="https://github.com/nousresearch/hermes-agent.git" +ARG HERMES_SOURCE_COMMIT="1388cd1c0c1800078bfcc92aebd144fbf145fdb4" # --- Building Stage --- -FROM ${BASE_NAMESPACE:+$BASE_NAMESPACE/}${BASE_IMG_BUILD} AS builder +FROM ${HERMES_BUILD_BASE_IMAGE:-${BASE_NAMESPACE:+$BASE_NAMESPACE/}${BASE_IMG_BUILD}} AS builder + +ARG HERMES_SOURCE_REPOSITORY +ARG HERMES_SOURCE_COMMIT # Build-time environment ENV NODE_ENV=development @@ -21,9 +31,18 @@ COPY work /opt/utils/ # Install build-time system dependencies (compilers + native libs needed for Python extensions). # Without these, `uv sync` fails when compiling packages like `matrix-*-crypto`, `cryptography`, or `ffi`-based wheels on cold builds. RUN set -eux \ + && printf 'Acquire::Retries "5";\nAcquire::http::Timeout "30";\n' > /etc/apt/apt.conf.d/80-labnow-retries \ && . /opt/utils/script-utils.sh && install_apt /opt/utils/install_list_hermes.apt \ + && rm -f /etc/apt/apt.conf.d/80-labnow-retries \ ## Clone source (full clone for reproducibility; depth 1 for speed) - && git clone --depth 1 --branch main https://github.com/nousresearch/hermes-agent.git . \ + && test "$(printf '%s' "$HERMES_SOURCE_COMMIT" | wc -c | tr -d ' ')" = 40 \ + && git init . \ + && git remote add origin "$HERMES_SOURCE_REPOSITORY" \ + && git fetch --depth 1 origin "$HERMES_SOURCE_COMMIT" \ + && git checkout --detach FETCH_HEAD \ + && test "$(git rev-parse HEAD)" = "$HERMES_SOURCE_COMMIT" \ + && printf '%s\n' "$HERMES_SOURCE_REPOSITORY" > /opt/hermes/.labnow-source-repository \ + && printf '%s\n' "$HERMES_SOURCE_COMMIT" > /opt/hermes/.labnow-source-commit \ && chmod +x /opt/utils/*.sh && mv /opt/utils/*hermes*.sh /opt/utils/install_list_hermes.apt /opt/utils/supervisord.conf /opt/hermes/ \ ## ---------- hack python-olm for building compatible wheels ---------- && mkdir -pv /opt/hermes/vendor \ @@ -31,7 +50,7 @@ RUN set -eux \ && curl -s https://pypi.org/pypi/python-olm/3.2.16/json \ | jq -r '.urls[] | select(.packagetype=="sdist").url' \ | xargs curl -L -o python-olm-3.2.16.tar.gz \ - && tar xf python-olm-3.2.16.tar.gz && cd python-olm-3.2.16 \ + && python -c 'import tarfile; tarfile.open("python-olm-3.2.16.tar.gz").extractall(path=".", filter="data")' && cd python-olm-3.2.16 \ && sed -i 's/cmake_minimum_required(VERSION [0-9.]*)/cmake_minimum_required(VERSION 3.5)/' libolm/CMakeLists.txt \ && pip wheel . --no-build-isolation -w /tmp/olm/wheels \ && mv /tmp/olm/wheels/*olm*.whl /opt/hermes/vendor/ \ @@ -58,9 +77,18 @@ RUN set -eux \ && printf 'docker\n' > /opt/hermes/.install_method ### --- Runtime Stage --- -FROM ${BASE_NAMESPACE:+$BASE_NAMESPACE/}${BASE_IMG} +FROM ${HERMES_RUNTIME_BASE_IMAGE:-${BASE_NAMESPACE:+$BASE_NAMESPACE/}${BASE_IMG}} + +ARG HERMES_SOURCE_REPOSITORY +ARG HERMES_SOURCE_COMMIT +ARG HERMES_BUILD_BASE_IMAGE +ARG HERMES_RUNTIME_BASE_IMAGE LABEL maintainer="postmaster@labnow.ai" +LABEL org.opencontainers.image.source="${HERMES_SOURCE_REPOSITORY}" +LABEL org.opencontainers.image.revision="${HERMES_SOURCE_COMMIT}" +LABEL io.labnow.hermes.build-base="${HERMES_BUILD_BASE_IMAGE}" +LABEL io.labnow.hermes.runtime-base="${HERMES_RUNTIME_BASE_IMAGE}" # Production environment ENV NODE_ENV=production @@ -68,19 +96,32 @@ ENV PLAYWRIGHT_BROWSERS_PATH=/opt/hermes/.playwright ENV PYTHONPATH="/opt/hermes:${PYTHONPATH:-}" ENV HERMES_HOME=/root/.hermes ENV HERMES_ALLOW_ROOT_GATEWAY=1 +# The Dashboard PTY starts the already-built ui-tui bundle with `node`. The +# runtime base is intentionally Python-only, so copy the architecture-matched +# Node runtime produced by the builder instead of lazily downloading one after +# an operator opens Chat. +ENV PATH="/opt/node/bin:${PATH}" # Copy the full hermes install tree from the builder (source + browsers + built frontends) COPY --from=builder /opt/hermes /opt/hermes +# `/opt/node` contains node, npm and the Node runtime's bundled execution +# material. Both stages use the same Docker target platform. +COPY --from=builder /opt/node /opt/node # Discover the real python site-packages so legacy env-var fallbacks point at the right tree. # Keep explicit versioned fallbacks around in case detection runs before the first pip install. RUN set -eux && cd /opt/hermes \ + && printf 'Acquire::Retries "5";\nAcquire::http::Timeout "30";\n' > /etc/apt/apt.conf.d/80-labnow-retries \ && . /opt/utils/script-utils.sh && install_apt /opt/hermes/install_list_hermes.apt \ + && rm -f /etc/apt/apt.conf.d/80-labnow-retries \ && uv pip install ./vendor/*.whl && rm -rf ./vendor \ && uv pip install -e ".[all,messaging,anthropic,bedrock,azure-identity,hindsight,matrix]" \ && rm -rf /opt/hermes/bin \ && ln -sf /opt/hermes/start-hermes.sh /opt/conda/bin/hermes /usr/local/bin/ \ && . /opt/utils/script-setup-sys.sh && setup_supervisord \ && mkdir -pv /etc/supervisord/ && mv /opt/hermes/supervisord.conf /etc/supervisord/supervisord.conf \ + && node --version \ + && test -s /opt/hermes/ui-tui/dist/entry.js \ + && node --check /opt/hermes/ui-tui/dist/entry.js \ && install__clean # Data persistence is owned by the runtime orchestrator. diff --git a/docker_keycloak/demo/docker-compose.yml b/docker_keycloak/compose/docker-compose.yml similarity index 100% rename from docker_keycloak/demo/docker-compose.yml rename to docker_keycloak/compose/docker-compose.yml diff --git a/docker_litellm/README.md b/docker_litellm/README.md index 45389bb..08e24ef 100644 --- a/docker_litellm/README.md +++ b/docker_litellm/README.md @@ -1,37 +1,104 @@ # LiteLLM Proxy -`litellm` is a lightweight proxy server to call 100+ LLM APIs using the OpenAI format, with a built-in UI dashboard. +本目录维护 LiteLLM 的部署适配,不 fork 或修改 LiteLLM 上游业务逻辑。提供可复现的本地基线:共享 PostgreSQL、共享 Redis、单副本与双副本 LiteLLM;凭据不写入仓库。 ---- +默认不构建 LiteLLM Dashboard 静态资源:固定源码在当前构建基础镜像上导出 `/_not-found` 时失败,而代理 API 与管理面不依赖该资源。需要 Dashboard 时可显式传入 `--build-arg BUILD_DASHBOARD=true` 单独处理该上游前端兼容性。 -## 1. Port Configuration +## 固定版本与镜像 -- **`4000` (HTTP)**: Serves the OpenAI-compatible REST API endpoints and the admin control panel dashboard interface. +| 项目 | 固定值 | 用途 | +| --- | --- | --- | +| LiteLLM 源码 | `v1.97.0-dev.1` / `ead62528e607b9d8e61273def638799c9c3a69ba` | Dockerfile 精确 fetch 并校验 HEAD | +| FastAPI | `0.136.3` | 固定到该 LiteLLM commit 仍使用 `get_flat_dependant` 的兼容版本 | +| Prisma Python client | `0.15.0` | LiteLLM 连接 PostgreSQL 所需客户端,兼容基础镜像的 Python 3.13 | +| 本地产物镜像 | `quay.io/labnow/litellm:1.97.0-ead62528e607` | Compose 的默认 LiteLLM 镜像 | +| PostgreSQL | `postgres:17-alpine@sha256:742f40ea20b9ff2ff31db5458d127452988a2164df9e17441e191f3b72252193` | 用户、凭证、模型、虚拟 key 与 spend 持久化 | +| Redis | `redis:7.4-alpine@sha256:e7723ff73d963f5cc6d9c4643ea3d989527a402a319239054e9472a7fb9219a2` | 副本共享认证缓存、RPM/TPM limiter 与协调缓存;SpendLog 的事实源是 PostgreSQL | ---- +镜像构建会对 wheel 自带的 LiteLLM Prisma schema 运行 `prisma generate`,并把生成的查询引擎固定在 `/opt/litellm/.cache`;没有该步骤,或将该缓存随 `/root/.cache` 清理,代理会在 PostgreSQL startup 时报缺少 Prisma binaries 或无法连接查询引擎。 -## 2. Data Persistence & Configurations +必须通过根目录 `tool.sh` 构建,避免基础镜像退回 Docker Hub: -LiteLLM looks for `config.yaml` in its home directory at startup: +```bash +export REGISTRY_SRC=quay.io +export REGISTRY_DST=quay.io +export CI_PROJECT_NAME=LabNow/lab-dev +source ./tool.sh +build_image_no_tag litellm 1.97.0-ead62528e607 docker_litellm/litellm.Dockerfile +``` + +构建完成后记录本地 digest: + +```bash +docker image inspect quay.io/labnow/litellm:1.97.0-ead62528e607 \ + --format 'image_id={{.Id}} created={{.Created}}' +``` -- **`/opt/litellm`**: Sourced workspace directory (configured via `HOME_LITELLM`). This is where `config.yaml` is written and read. -- **`/root/workspace`**: Additional shared data directories volume. +## 本地启动 -### Custom Home Directory -You can override the home location using the environment variable: -- `HOME_LITELLM`: Paths to store the active configs (e.g. `/root/workspace`). +准备不会被 Git 跟踪的配置。不要把 `.env` 发送到聊天、日志或提交中。 ---- +```bash +cd docker_litellm/compose +cp .env.example .env +# 在 .env 中生成并填写 LITELLM_MASTER_KEY、POSTGRES_PASSWORD、REDIS_PASSWORD。 +# 真实上游调用另行填写 UPSTREAM_PROVIDER、UPSTREAM_API_KEY、UPSTREAM_BASE_URL、UPSTREAM_MODEL。 +docker compose --env-file .env -f docker-compose.litellm.yml --profile single up -d +``` -## 3. Quickstart Example +Compose 的项目、显式容器和外部网络均以仓库既有的 `PROFILE_ENV` 推导,默认实例为 +`litellm-baseline`:Compose project 为 `litellm-baseline-svc-litellm`,网络为 +`litellm-baseline-svc-litellm-net`。若需与另一套本地 LiteLLM 基线并行运行,在同一条命令前 +设置不同实例名;不要混用 `-p` 或 `COMPOSE_PROJECT_NAME`,以免项目名与显式容器/网络命名源分离。 -Run LiteLLM Proxy with mapped configuration folder: ```bash -docker run -d \ - --name svc-litellm \ - -p 4000:4000 \ - -v /path/to/your/config:/opt/litellm \ - labnow/litellm:latest +PROFILE_ENV=litellm-dev-a docker compose --env-file .env -f docker-compose.litellm.yml --profile single up -d +PROFILE_ENV=litellm-dev-b docker compose --env-file .env -f docker-compose.litellm.yml --profile single up -d +``` + +迁移与代理启动刻意分离:先用 `./scripts/run-migration.sh` 执行数据库迁移(migration profile),再启动代理 profile;不要把 migration profile 与代理 profile 放入同一条 `up` 命令。 + +默认端口只发布在 `127.0.0.1`:副本 1 为 `4000`,副本 2 为 `4001`。PostgreSQL 与 Redis 不发布宿主机端口。停止服务不会删除卷;如需删除本地数据,先人工确认后使用 `docker compose ... down -v`。 + +## 配置与安全边界 + +`config.yaml` 从运行时环境读取管理面 `LITELLM_MASTER_KEY`、`DATABASE_URL` 与 Redis 凭据。Compose 不把管理密钥、数据库密码或含密码的连接串写入服务 `environment`:它将 `LITELLM_MASTER_KEY`、`POSTGRES_PASSWORD` 和 `REDIS_PASSWORD` 交给 Docker Secret;PostgreSQL 使用官方 `POSTGRES_PASSWORD_FILE`,LiteLLM 的 `start-litellm.sh` 在最终 `exec` 前读取 Secret 文件、构造 `DATABASE_URL` 并立即转交 LiteLLM。管理面 key 仅用于 `/user/new`、`/credentials`、`/model/new`、`/key/generate`、`/key/block` 和 `/key/delete` 等管理接口;数据面虚拟 key 应为短期、模型白名单、TTL、预算、RPM、TPM 与 `llm_api` 路由限制的独立 key。双副本基线启用 `enable_redis_auth_cache`,并将 `user_api_key_cache_ttl` 设为 1 秒,以使撤销在 30 秒内经共享 Redis 重新校验。 + +| 变量 | 是否必填 | 作用 | 风险说明 | +| --- | ---: | --- | --- | +| `LITELLM_MASTER_KEY` | 是 | 管理面认证 | 仅放在忽略的 `.env` 或部署 Secret | +| `POSTGRES_PASSWORD` | 是 | PostgreSQL 密码 | 仅限本地测试或部署 Secret | +| `REDIS_PASSWORD` | 是 | Redis 认证 | 仅限本地测试或部署 Secret | +| `UPSTREAM_API_KEY` | 真实调用时是 | 上游模型凭据 | 仅由本地验证客户端读取;不会注入 LiteLLM 容器、不提交、不打印 | +| `UPSTREAM_PROVIDER` | 真实调用时是 | 上游 provider 选择 | 当前明确支持 `deepseek` | +| `UPSTREAM_BASE_URL` | 真实调用时是 | OpenAI 兼容上游地址 | 由环境决定 | +| `UPSTREAM_MODEL` | 真实调用时是 | 上游模型名 | 用于创建测试模型 | +| `REDIS_CIRCUIT_BREAKER_RECOVERY_TIMEOUT` | `5` | Redis 断连后的 LiteLLM 缓存恢复探测窗口(秒) | 恢复期间管理面可能暂时返回 500 | + +Compose 凭据边界的残余风险:LiteLLM 上游配置接口仍要求 `LITELLM_MASTER_KEY` 与 `DATABASE_URL` 在其最终进程环境中可见;本基线已接受这一点。凭据不出现在 Compose 渲染、容器 `docker inspect` metadata、命令行参数、容器日志或运行时临时文件中。使用具有 Docker daemon 访问权限或容器内同等调试权限的主体仍应视为高权限主体,不应以该边界替代主机与容器访问控制。 + +## SpendLogs 数据保留 + +`LiteLLM_SpendLogs` 是 LiteLLM 计量明细的事实源。labnow-shell 的用量观测从 `/spend/logs/v2` 按水位线增量摄入并写入自有聚合表;某一历史时间段已成功摄入后,该时间段的 SpendLogs 行可以清理,清理不会影响已写入的下游聚合表。清理前仍须确认对应水位线已持久化、聚合写入成功,且没有其他仍依赖该明细表的消费者。 + +本仓固定的 LiteLLM `v1.97.0-dev.1` / `ead62528e607b9d8e61273def638799c9c3a69ba` 已支持在 `general_settings` 中设置 `maximum_spend_logs_retention_period`。经配置变更评审后,可使用以下配置启用 LiteLLM 自带的定时清理;`"7d"` 表示删除早于当前时间 7 天的 SpendLogs。当前基线**未**启用该项,本批次仅登记策略,不修改 `config.yaml`。 + +```yaml +general_settings: + maximum_spend_logs_retention_period: "7d" ``` -By default, it will look for a `config.yaml` in the directory. If not found, a basic template targeting `gpt-3.5-turbo` is auto-generated. +该固定 commit 的 [`SpendLogCleanup`](https://github.com/BerriAI/litellm/blob/ead62528e607b9d8e61273def638799c9c3a69ba/litellm/proxy/db/db_transaction_queue/spend_log_cleanup.py#L53-L75) 会读取此配置并按保留期计算清理边界;[`proxy_server.py`](https://github.com/BerriAI/litellm/blob/ead62528e607b9d8e61273def638799c9c3a69ba/litellm/proxy/proxy_server.py#L8318-L8353) 会在该配置存在时注册清理任务(未另行配置时按 `1d` 间隔执行)。 + +保留窗口必须大于下游增量摄入的端到端延迟,包括分钟级延迟、任务重试和短暂故障恢复时间;建议至少保留 **7 天**。不要以一次成功的摄入为依据立即清理最近数据,以免水位线尚未推进或重试中的批次出现缺口。 + +## Readiness 与 Redis 说明 + +LiteLLM `v1.97.0-dev.1` 的公开 `/health/readiness` 仅返回服务与数据库连通性,不将 Redis 纳入公开 readiness。因此 Compose 健康检查只能确认 LiteLLM + PostgreSQL;可额外从每个 LiteLLM 容器执行 Redis `PING` 确认。若 Redis 不可用,多副本认证缓存、RPM/TPM limiter 与协调结论无效,不能宣称为高可用。Redis 恢复后,LiteLLM 的认证缓存 circuit breaker 需要经过 `REDIS_CIRCUIT_BREAKER_RECOVERY_TIMEOUT` 后才会重新探测,默认 5 秒。跨副本 SpendLog 只证明 PostgreSQL 可见性,不是 Redis Spend counter 或预算准入控制证据。 + +## 常见问题 + +- `LITELLM_MASTER_KEY` 或数据库密码缺失:先检查被忽略的 `compose/.env`,不要将其内容贴出。 +- readiness 未连接数据库:查看 `docker compose ... logs postgres litellm-1`,并保留卷以便排查迁移。 +- Redis 探针失败:先确认 `redis` health 与密码一致,再做双副本撤销验证。 +- 上游调用:仅在 `.env` 中提供专用、低权限、可轮换的测试 key。 diff --git a/docker_litellm/compose/.env.example b/docker_litellm/compose/.env.example new file mode 100644 index 0000000..e47def0 --- /dev/null +++ b/docker_litellm/compose/.env.example @@ -0,0 +1,27 @@ +# Copy this file to docker_litellm/compose/.env. It is intentionally ignored. +# Do not commit real API keys, management keys, passwords, or virtual keys. + +TZ=Asia/Hong_Kong + +# Build this exact source baseline through ../../tool.sh before running Compose. +LITELLM_IMAGE=quay.io/labnow/litellm:1.97.0-ead62528e607 +LITELLM_1_CONTAINER_NAME=svc-litellm-1 +LITELLM_2_CONTAINER_NAME=svc-litellm-2 +LITELLM_PUBLISH_HOST=127.0.0.1 +LITELLM_1_PORT=4000 +LITELLM_2_PORT=4001 + +# Local-only secrets. Generate unique values; these examples are placeholders. +LITELLM_MASTER_KEY= +POSTGRES_DB=litellm +POSTGRES_USER=litellm +POSTGRES_PASSWORD= +REDIS_PASSWORD= + +# Optional upstream used only by local verification clients; it is never injected +# into the LiteLLM containers. Supported provider: deepseek. +# Keep UPSTREAM_API_KEY empty to validate infrastructure paths only. +UPSTREAM_PROVIDER=deepseek +UPSTREAM_API_KEY= +UPSTREAM_BASE_URL=https://api.deepseek.com/v1 +UPSTREAM_MODEL=deepseek-v4-flash diff --git a/docker_litellm/compose/config.migrate.yaml b/docker_litellm/compose/config.migrate.yaml new file mode 100644 index 0000000..91e27f6 --- /dev/null +++ b/docker_litellm/compose/config.migrate.yaml @@ -0,0 +1,20 @@ +# Dedicated one-shot migration configuration. Keep this in sync with +# config.yaml; only this job is permitted to apply Prisma migrations. +model_list: [] + +general_settings: + master_key: os.environ/LITELLM_MASTER_KEY + database_url: os.environ/DATABASE_URL + store_model_in_db: true + disable_spend_logs: false + disable_prisma_schema_update: false + +litellm_settings: + turn_off_message_logging: true + cache: true + enable_redis_auth_cache: true + cache_params: + type: redis + host: os.environ/REDIS_HOST + port: os.environ/REDIS_PORT + password: os.environ/REDIS_PASSWORD diff --git a/docker_litellm/compose/config.yaml b/docker_litellm/compose/config.yaml new file mode 100644 index 0000000..f828b0d --- /dev/null +++ b/docker_litellm/compose/config.yaml @@ -0,0 +1,33 @@ +# Runtime configuration. Secrets only come from environment variables. +# The management API persists users, credentials, model records and virtual keys +# in PostgreSQL. Redis is the shared cache/co-ordination backend for replicas. + +model_list: [] + +general_settings: + master_key: os.environ/LITELLM_MASTER_KEY + database_url: os.environ/DATABASE_URL + store_model_in_db: true + # Keep per-replica virtual-key authorization state short-lived so a revoked + # key is revalidated through the shared cache within the 30 s smoke SLO. + user_api_key_cache_ttl: 1 + # Preserve request metadata needed for metering, but do not configure prompt + # or completion-content logging in this local baseline. + disable_spend_logs: false + # Proxy replicas only check the schema. The dedicated migration job uses + # config.migrate.yaml and is the sole process allowed to apply migrations. + disable_prisma_schema_update: true + +litellm_settings: + # LiteLLM 1.97.0 reads this from litellm_settings and redacts request and + # response content before standard/spend logging. Metering fields remain. + turn_off_message_logging: true + cache: true + # Make virtual-key authorization cache state visible to every LiteLLM + # replica. Without this, each replica keeps an isolated in-memory key cache. + enable_redis_auth_cache: true + cache_params: + type: redis + host: os.environ/REDIS_HOST + port: os.environ/REDIS_PORT + password: os.environ/REDIS_PASSWORD diff --git a/docker_litellm/compose/docker-compose.litellm.yml b/docker_litellm/compose/docker-compose.litellm.yml new file mode 100644 index 0000000..b710c0f --- /dev/null +++ b/docker_litellm/compose/docker-compose.litellm.yml @@ -0,0 +1,139 @@ +# Keep the project, explicit container names, and external network names in +# the same instance namespace. This follows the repository PROFILE_ENV +# convention and allows multiple local LiteLLM stacks to coexist. +name: ${PROFILE_ENV:-litellm-baseline}-svc-litellm + +x-litellm-common: &litellm-common + image: ${LITELLM_IMAGE:?set LITELLM_IMAGE to the locally built quay.io/labnow/litellm image} + pull_policy: never + restart: "no" + environment: + TZ: ${TZ:-Asia/Hong_Kong} + LITELLM_MASTER_KEY_FILE: /run/secrets/litellm_master_key + POSTGRES_PASSWORD_FILE: /run/secrets/postgres_password + POSTGRES_HOST: postgres + POSTGRES_PORT: "5432" + POSTGRES_DB: ${POSTGRES_DB:-litellm} + POSTGRES_USER: ${POSTGRES_USER:?set in .env} + REDIS_HOST: redis + REDIS_PORT: "6379" + REDIS_PASSWORD_FILE: /run/secrets/redis_password + REDIS_CIRCUIT_BREAKER_RECOVERY_TIMEOUT: ${REDIS_CIRCUIT_BREAKER_RECOVERY_TIMEOUT:-5} + STORE_PROMPTS_IN_SPEND_LOGS: "false" + STORE_MODEL_IN_DB: "True" + LITELLM_LOG: ${LITELLM_LOG:-INFO} + volumes: + - ./config.yaml:/opt/litellm/config.yaml:ro + - ./config.migrate.yaml:/opt/litellm/config.migrate.yaml:ro + - ../work/start-litellm.sh:/opt/utils/start-litellm.sh:ro + - ../work/run-migration-locked.py:/opt/utils/run-migration-locked.py:ro + secrets: + - litellm_master_key + - postgres_password + - redis_password + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + networks: + - litellm-net + healthcheck: + test: ["CMD-SHELL", "python3 -c \"import urllib.request; urllib.request.urlopen('http://127.0.0.1:4000/health/readiness', timeout=3)\""] + interval: 10s + timeout: 5s + retries: 18 + start_period: 30s + +services: + # Run LiteLLM's own migration-only mode explicitly before proxy replicas. + # Replicas intentionally do not depend on this profile service: the deploy + # workflow calls scripts/run-migration.sh after postgres/redis are healthy. + # Prisma's migration table makes repeated one-shot jobs idempotent. + litellm-migrate: + <<: *litellm-common + container_name: ${LITELLM_MIGRATE_CONTAINER_NAME:-${PROFILE_ENV:-litellm-baseline}-svc-litellm-migrate} + profiles: ["migrate"] + command: ["/opt/utils/start-litellm.sh", "python3", "/opt/utils/run-migration-locked.py", "--config", "config.migrate.yaml", "--skip_server_startup", "--enforce_prisma_migration_check"] + healthcheck: + disable: true + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + + postgres: + image: postgres:17-alpine@sha256:742f40ea20b9ff2ff31db5458d127452988a2164df9e17441e191f3b72252193 + pull_policy: never + restart: "no" + environment: + POSTGRES_DB: ${POSTGRES_DB:-litellm} + POSTGRES_USER: ${POSTGRES_USER:?set in .env} + POSTGRES_PASSWORD_FILE: /run/secrets/postgres_password + secrets: + - postgres_password + volumes: + - litellm_postgres_data:/var/lib/postgresql/data + networks: + - litellm-net + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"] + interval: 5s + timeout: 5s + retries: 20 + + redis: + image: redis:7.4-alpine@sha256:e7723ff73d963f5cc6d9c4643ea3d989527a402a319239054e9472a7fb9219a2 + pull_policy: never + restart: "no" + secrets: + - redis_password + tmpfs: + - /run/redis:mode=0700 + command: + - /bin/sh + - -ec + - >- + umask 077; + { printf 'appendonly yes\nrequirepass '; cat /run/secrets/redis_password; printf '\n'; } > /run/redis/redis.conf; + exec redis-server /run/redis/redis.conf + volumes: + - litellm_redis_data:/data + networks: + - litellm-net + healthcheck: + test: ["CMD-SHELL", "REDISCLI_AUTH=\"$$(cat /run/secrets/redis_password)\" redis-cli --no-auth-warning ping | grep -qx PONG"] + interval: 5s + timeout: 5s + retries: 20 + + litellm-1: + <<: *litellm-common + container_name: ${LITELLM_1_CONTAINER_NAME:-${PROFILE_ENV:-litellm-baseline}-svc-litellm-1} + profiles: ["single", "ha"] + ports: + - "${LITELLM_PUBLISH_HOST:-127.0.0.1}:${LITELLM_1_PORT:-4000}:4000" + + litellm-2: + <<: *litellm-common + container_name: ${LITELLM_2_CONTAINER_NAME:-${PROFILE_ENV:-litellm-baseline}-svc-litellm-2} + profiles: ["ha"] + ports: + - "${LITELLM_PUBLISH_HOST:-127.0.0.1}:${LITELLM_2_PORT:-4001}:4000" + +volumes: + litellm_postgres_data: + litellm_redis_data: + +secrets: + litellm_master_key: + environment: LITELLM_MASTER_KEY + postgres_password: + environment: POSTGRES_PASSWORD + redis_password: + environment: REDIS_PASSWORD + +networks: + litellm-net: + name: ${PROFILE_ENV:-litellm-baseline}-svc-litellm-net diff --git a/docker_litellm/compose/scripts/run-migration.sh b/docker_litellm/compose/scripts/run-migration.sh new file mode 100755 index 0000000..4f5365f --- /dev/null +++ b/docker_litellm/compose/scripts/run-migration.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +# Apply LiteLLM Prisma migrations explicitly, once per deployment operation. +# Proxy replicas deliberately do not depend on this one-shot Compose service. +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +compose_dir="$(cd "${script_dir}/.." && pwd)" +source "${script_dir}/verification-lib.sh" +env_file="${LITELLM_SMOKE_ENV_FILE:-${compose_dir}/.env}" +compose=(docker compose --env-file "$env_file" -f "${compose_dir}/docker-compose.litellm.yml") +summary_file="${LITELLM_MIGRATION_SUMMARY_FILE:-${compose_dir}/artifacts/p1-migration-summary.json}" + +result="failed" +phase="initializing" +image_ref="" +tmpdir="" +verification_run_id="${VERIFICATION_RUN_ID:-standalone}" +verification_invalidate_report "$summary_file" +cleanup() { + local exit_code=$? summary_tmp + trap - EXIT + umask 077 + mkdir -p "$(dirname "$summary_file")" + chmod 700 "$(dirname "$summary_file")" + summary_tmp="${summary_file}.tmp.$$" + jq -n \ + --arg commit "$(git -C "$compose_dir/../.." rev-parse HEAD)" \ + --arg image_id "$(docker image inspect "$image_ref" --format '{{.Id}}' 2>/dev/null || true)" \ + --arg tested_at "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ + --arg run_id "$verification_run_id" --arg result "$result" --arg phase "$phase" --argjson exit_code "$exit_code" \ + '{verification_run_id:$run_id,commit:$commit,image_id:$image_id,tested_at:$tested_at,mode:"migration",result:$result,phase:$phase,exit_code:$exit_code,proxy_replicas_started:false,content_redacted:true}' \ + > "$summary_tmp" && chmod 600 "$summary_tmp" && mv "$summary_tmp" "$summary_file" || { + rm -f "$summary_tmp" + exit_code=1 + } + [[ -z "$tmpdir" ]] || rm -rf "$tmpdir" + return "$exit_code" +} +trap cleanup EXIT + +[[ -f "$env_file" ]] || { echo "missing ignored local environment file" >&2; exit 2; } +umask 077 +tmpdir="$(mktemp -d "${TMPDIR:-/tmp}/litellm-migration.XXXXXX")" +chmod 700 "$tmpdir" +verification_prepare_environment "$env_file" "${compose_dir}/docker-compose.litellm.yml" "$tmpdir" +image_ref="$(verification_env LITELLM_IMAGE)" +: "${image_ref:?missing LITELLM_IMAGE in effective Compose environment}" + +# A cold Compose start previously raced PostgreSQL/Redis readiness. `--wait` +# makes the dependency condition explicit before the one-shot job is run. +phase="waiting_dependencies" +"${compose[@]}" up -d --wait postgres redis +phase="migration_job" +"${compose[@]}" --profile migrate run --rm --no-deps litellm-migrate +phase="completed" +result="passed" +echo "PASS migration: dependencies healthy; migration-only job completed; proxy replicas were not started." diff --git a/docker_litellm/compose/scripts/verification-lib.sh b/docker_litellm/compose/scripts/verification-lib.sh new file mode 100755 index 0000000..b94f4d5 --- /dev/null +++ b/docker_litellm/compose/scripts/verification-lib.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +# Shared fail-closed helpers for the migration/verification scripts. Never source .env. + +verification_prepare_environment() { + local env_file="$1" compose_file="$2" work_dir="$3" + umask 077 + verification_environment_file="$work_dir/compose.environment" + docker compose --env-file "$env_file" -f "$compose_file" config --environment > "$verification_environment_file" + chmod 600 "$verification_environment_file" +} + +verification_env() { + local name="$1" + awk -F= -v name="$name" '$1 == name {sub(/^[^=]*=/, ""); print; exit}' "$verification_environment_file" +} + +verification_file_mode() { + local file="$1" platform + platform="$(uname -s)" + case "$platform" in + Darwin|FreeBSD|OpenBSD|NetBSD|DragonFly) + stat -f '%Lp' "$file" + ;; + Linux) + stat -c '%a' -- "$file" + ;; + *) + echo "unsupported platform for portable file mode check: $platform" >&2 + return 2 + ;; + esac +} + +verification_assert_file_mode() { + local file="$1" expected_mode="$2" description="$3" actual_mode + actual_mode="$(verification_file_mode "$file")" || return $? + [[ "$actual_mode" == "$expected_mode" ]] || { + echo "$description has mode $actual_mode, expected $expected_mode: $file" >&2 + return 1 + } +} + +verification_invalidate_report() { + local report="$1" + mkdir -p "$(dirname "$report")" + chmod 700 "$(dirname "$report")" + rm -f "$report" +} + +verification_new_run_id() { + python3 -c 'import secrets; print("p1-" + secrets.token_hex(16))' +} + +verification_assert_run_id() { + [[ "${VERIFICATION_RUN_ID:-}" =~ ^p1-[a-f0-9]{32}$ ]] || { + echo "VERIFICATION_RUN_ID must be generated by verify-p1.sh" >&2 + return 2 + } +} diff --git a/docker_litellm/demo/docker-compose.litellm.yml b/docker_litellm/demo/docker-compose.litellm.yml deleted file mode 100644 index 28d9383..0000000 --- a/docker_litellm/demo/docker-compose.litellm.yml +++ /dev/null @@ -1,19 +0,0 @@ -services: - svc-litellm: - container_name: svc-litellm - image: quay.io/labnow/litellm:latest - restart: unless-stopped - # networks: ["net-litellm"] - ports: - - "4000:4000" - volumes: - - ./data:/root/workspace - # environment: - # - LITELLM_MASTER_KEY=sk-1234 - # - OPENAI_API_KEY=your-openai-key - # - GEMINI_API_KEY=your-gemini-key - logging: - driver: "json-file" - options: - max-size: "10m" - max-file: "3" diff --git a/docker_litellm/litellm.Dockerfile b/docker_litellm/litellm.Dockerfile index 2ac0224..7bc6ec6 100644 --- a/docker_litellm/litellm.Dockerfile +++ b/docker_litellm/litellm.Dockerfile @@ -3,24 +3,36 @@ ARG BASE_NAMESPACE ARG BASE_IMG_BUILD="node" ARG BASE_IMG="base" +ARG LITELLM_REF="ead62528e607b9d8e61273def638799c9c3a69ba" +ARG BUILD_DASHBOARD="false" # --- Building Stage --- FROM ${BASE_NAMESPACE:+$BASE_NAMESPACE/}${BASE_IMG_BUILD} AS builder +ARG LITELLM_REF +ARG BUILD_DASHBOARD + LABEL maintainer="postmaster@labnow.ai" # Build-time environment ENV NODE_ENV=development WORKDIR /build -# Clone source, Build UI (Dashboard) & Build Python wheel in one RUN layer +# Clone the fixed source and build its Python wheel. Dashboard export is +# optional: the API proxy does not depend on the browser dashboard. RUN set -eux \ - && git clone --depth 1 --branch main https://github.com/BerriAI/litellm.git . \ - && cd ui/litellm-dashboard \ - && npm install \ - && npm run build \ - && mkdir -pv ../../litellm/proxy/_experimental/out \ - && cp -r out/* ../../litellm/proxy/_experimental/out/ \ + && git init . \ + && git remote add origin https://github.com/BerriAI/litellm.git \ + && git fetch --depth 1 origin "${LITELLM_REF}" \ + && git checkout --detach FETCH_HEAD \ + && test "$(git rev-parse HEAD)" = "${LITELLM_REF}" \ + && if [ "${BUILD_DASHBOARD}" = "true" ]; then \ + cd ui/litellm-dashboard \ + && npm install \ + && npm run build \ + && mkdir -pv ../../litellm/proxy/_experimental/out \ + && cp -r out/* ../../litellm/proxy/_experimental/out; \ + fi \ && cd /build \ && python3 -m pip install --upgrade pip build \ && python3 -m build --wheel --outdir dist @@ -39,13 +51,25 @@ WORKDIR ${HOME_LITELLM} # Copy utilities, tools and build artifacts COPY work /opt/utils/ COPY --from=builder /build/dist/*.whl /tmp/ +# prisma-python invokes Node again for migration operations. Copy the fixed +# builder runtime into the final image so a fresh migration container never +# tries to download Node during startup. +COPY --from=builder /opt/node /opt/node # Install Runtime dependencies and configure tools RUN set -eux \ && chmod +x /opt/utils/*.sh \ && ln -sf /opt/utils/start-litellm.sh /usr/local/bin/start-litellm.sh \ - && pip install --no-cache-dir /tmp/*.whl \ - && pip install --no-cache-dir 'litellm[proxy]' \ + && WHEEL="$(find /tmp -maxdepth 1 -name '*.whl' -print -quit)" \ + && test -n "${WHEEL}" \ + && pip install --no-cache-dir "${WHEEL}[proxy]" "fastapi==0.136.3" "prisma==0.15.0" \ + && python3 -c 'from fastapi.dependencies.utils import get_flat_dependant; import prisma' \ + && PRISMA_SCHEMA="$(python3 -c 'import pathlib, litellm; print(pathlib.Path(litellm.__file__).parent / "proxy" / "schema.prisma")')" \ + && test -f "${PRISMA_SCHEMA}" \ + # Keep the generated query engine outside /root: install__clean removes + # root-owned caches, while the runtime starts with HOME=/opt/litellm. + && PRISMA_HOME_DIR="${HOME_LITELLM}" prisma generate --schema "${PRISMA_SCHEMA}" \ + && test -d "${HOME_LITELLM}/.cache/prisma-python" \ ## Install supervisord (Go version) if needed or use simple entrypoint && source /opt/utils/script-setup-sys.sh && setup_supervisord \ && source /opt/utils/script-utils.sh && install__clean \ diff --git a/docker_litellm/work/run-migration-locked.py b/docker_litellm/work/run-migration-locked.py new file mode 100755 index 0000000..08aa384 --- /dev/null +++ b/docker_litellm/work/run-migration-locked.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +"""Run LiteLLM's migration mode under one PostgreSQL advisory lock. + +The lock connection is intentionally held while the child migration process +runs. Concurrent jobs therefore overlap as containers but cannot execute a +migration concurrently. No connection string or secret is printed. +""" + +import asyncio +import os +import re +import subprocess +import sys + +from prisma import Prisma + +LOCK_ID = 548_019_700_001 +DATABASE_URL_PATTERN = re.compile(r"postgres(?:ql)?://[^\s'\"`]+") + + +def redact_migration_output(value: str) -> str: + """Keep migration diagnostics while preventing connection strings in logs.""" + return DATABASE_URL_PATTERN.sub("postgresql://", value) + + +async def main() -> int: + db = Prisma() + await db.connect() + try: + print("P1_MIGRATION_LOCK_WAITING", flush=True) + # Prisma cannot deserialize PostgreSQL's `void` return from + # pg_advisory_lock(). Poll the boolean try-lock instead; this keeps + # the same session-scoped singleton guarantee and records real wait. + while True: + lock_result = await db.query_raw( + f"SELECT pg_try_advisory_lock({LOCK_ID}) AS acquired" + ) + if lock_result[0]["acquired"]: + break + await asyncio.sleep(0.1) + print("P1_MIGRATION_LOCK_ACQUIRED", flush=True) + hold_seconds = int(os.environ.get("LITELLM_MIGRATION_LOCK_HOLD_SECONDS", "0")) + if hold_seconds > 0: + print("P1_MIGRATION_LOCK_TEST_HOLD", flush=True) + await asyncio.sleep(hold_seconds) + print("P1_MIGRATION_EXECUTION_START", flush=True) + completed = subprocess.run( + ["/bin/bash", "/opt/utils/start-litellm.sh", *sys.argv[1:]], + check=False, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + errors="replace", + ) + if completed.stdout: + print(redact_migration_output(completed.stdout), end="", flush=True) + print("P1_MIGRATION_EXECUTION_DONE", flush=True) + return completed.returncode + finally: + try: + await db.query_raw(f"SELECT pg_advisory_unlock({LOCK_ID})") + print("P1_MIGRATION_LOCK_RELEASED", flush=True) + finally: + await db.disconnect() + + +if __name__ == "__main__": + raise SystemExit(asyncio.run(main())) diff --git a/docker_litellm/work/start-litellm.sh b/docker_litellm/work/start-litellm.sh old mode 100644 new mode 100755 index acf719c..74a69b3 --- a/docker_litellm/work/start-litellm.sh +++ b/docker_litellm/work/start-litellm.sh @@ -1,27 +1,77 @@ #!/usr/bin/env bash -set -eu +set -Eeuo pipefail + +config_tmp="" +cleanup() { + local exit_code=$? + trap - EXIT + if [[ -n "$config_tmp" && -e "$config_tmp" ]]; then + rm -f -- "$config_tmp" + fi + return "$exit_code" +} +trap cleanup EXIT # Setup workspace directory HOME_LITELLM="${HOME_LITELLM:-/opt/litellm}" mkdir -p "$HOME_LITELLM" export HOME="$HOME_LITELLM" +export PRISMA_HOME_DIR="${PRISMA_HOME_DIR:-$HOME_LITELLM}" cd "$HOME_LITELLM" -# Default config if not exists +# Compose mounts credentials as Docker secrets. Read them only in this process +# tree, immediately before the final exec: Docker metadata and argv therefore +# contain neither secret values nor a password-bearing DATABASE_URL. LiteLLM +# itself requires the management key and DATABASE_URL in its final environment; +# that process-environment visibility is the explicitly accepted residual risk. +read_secret_file() { + local variable_name="$1" secret_file="$2" + test -r "$secret_file" + export "$variable_name=$(cat "$secret_file")" +} + +if [ -n "${LITELLM_MASTER_KEY_FILE:-}" ]; then + read_secret_file LITELLM_MASTER_KEY "$LITELLM_MASTER_KEY_FILE" +fi + +if [ -n "${POSTGRES_PASSWORD_FILE:-}" ]; then + read_secret_file POSTGRES_PASSWORD "$POSTGRES_PASSWORD_FILE" + : "${POSTGRES_USER:?POSTGRES_USER is required with POSTGRES_PASSWORD_FILE}" + export DATABASE_URL="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST:-postgres}:${POSTGRES_PORT:-5432}/${POSTGRES_DB:-litellm}" + unset POSTGRES_PASSWORD +fi + +if [ -n "${REDIS_PASSWORD_FILE:-}" ]; then + read_secret_file REDIS_PASSWORD "$REDIS_PASSWORD_FILE" +fi + +# LiteLLM checks this environment variable while serializing SpendLog payloads. +# Keep metering enabled in config.yaml, but never persist prompt content. +export STORE_PROMPTS_IN_SPEND_LOGS="${STORE_PROMPTS_IN_SPEND_LOGS:-false}" + +# Default config if not exists. The Compose baseline always mounts an +# explicit config with PostgreSQL and Redis; this fallback remains only for +# backwards-compatible standalone use. if [ ! -f "config.yaml" ]; then echo "Creating default config.yaml..." - cat < config.yaml + umask 077 + config_tmp="$(mktemp "${HOME_LITELLM}/config.yaml.tmp.XXXXXX")" + chmod 600 "$config_tmp" + cat <<'EOF' > "$config_tmp" model_list: - model_name: gpt-3.5-turbo litellm_params: model: gpt-3.5-turbo EOF + chmod 600 "$config_tmp" + mv -f -- "$config_tmp" config.yaml + config_tmp="" fi # If no arguments are passed, start litellm proxy with defaults if [ $# -eq 0 ]; then - set -- --config config.yaml --port 4000 --host 0.0.0.0 + set -- --config config.yaml --port "${LITELLM_PORT:-4000}" --host "${LITELLM_HOST:-0.0.0.0}" fi # Route execution: run command directly if it exists, otherwise wrap with litellm diff --git a/docker_nocobase/demo/README.md b/docker_nocobase/compose/README.md similarity index 98% rename from docker_nocobase/demo/README.md rename to docker_nocobase/compose/README.md index 9ac38de..ed644c3 100644 --- a/docker_nocobase/demo/README.md +++ b/docker_nocobase/compose/README.md @@ -1,6 +1,6 @@ # NocoBase 物理表建表与元数据 (Metadata) 配置指南 -本目录包含用于重构及初始化 CRM 业务系统的 PostgreSQL DDL 脚本 [nocobase-crm.sql](docker_nocobase/demo/nocobase-crm.sql)。 +本目录包含用于重构及初始化 CRM 业务系统的 PostgreSQL DDL 脚本 [nocobase-crm.sql](docker_nocobase/compose/nocobase-crm.sql)。 为了避免后续在直接修改数据库元数据或创建物理表时导致 NocoBase 报错(例如:*“当数据表没有主键时...”* 或页面区块选择器无法选取数据表),请严格遵循以下设计原则与最佳实践。 diff --git a/docker_nocobase/demo/docker-compose.yml b/docker_nocobase/compose/docker-compose.yml similarity index 100% rename from docker_nocobase/demo/docker-compose.yml rename to docker_nocobase/compose/docker-compose.yml diff --git a/docker_nocobase/demo/nocobase-crm.sql b/docker_nocobase/compose/nocobase-crm.sql similarity index 100% rename from docker_nocobase/demo/nocobase-crm.sql rename to docker_nocobase/compose/nocobase-crm.sql diff --git a/docker_openclaw/demo/docker-compose.yml b/docker_openclaw/compose/docker-compose.yml similarity index 100% rename from docker_openclaw/demo/docker-compose.yml rename to docker_openclaw/compose/docker-compose.yml diff --git a/docker_searxng/demo/docker-compose.searxng-standalone.yml b/docker_searxng/compose/docker-compose.searxng-standalone.yml similarity index 100% rename from docker_searxng/demo/docker-compose.searxng-standalone.yml rename to docker_searxng/compose/docker-compose.searxng-standalone.yml diff --git a/docker_searxng/demo/docker-compose.searxng-with-proxy.yml b/docker_searxng/compose/docker-compose.searxng-with-proxy.yml similarity index 100% rename from docker_searxng/demo/docker-compose.searxng-with-proxy.yml rename to docker_searxng/compose/docker-compose.searxng-with-proxy.yml diff --git a/docker_searxng/demo/searxng/settings.yml b/docker_searxng/compose/searxng/settings.yml similarity index 100% rename from docker_searxng/demo/searxng/settings.yml rename to docker_searxng/compose/searxng/settings.yml diff --git a/tool.sh b/tool.sh index c65cc7a..9e7fa28 100644 --- a/tool.sh +++ b/tool.sh @@ -12,7 +12,15 @@ CI_PROJECT_SPACE=$(echo "${CI_PROJECT_BRANCH}" | cut -f1 -d'/') # If on the main branch, image namespace will be same as CI_PROJECT_NAME's name space; # else (not main branch), image namespace = {CI_PROJECT_NAME's name space} + "0" + {1st substr before / in CI_PROJECT_SPACE}. -[ "${CI_PROJECT_BRANCH}" = "main" ] && NAMESPACE_SUFFIX="" || NAMESPACE_SUFFIX="0${CI_PROJECT_SPACE}" ; +# A local LabNow service build is an intentionally local-only artifact, but it +# must keep the stable `labnow` namespace used by each service's Compose file. +# CI keeps the historical branch-suffixed namespace behaviour unchanged. This +# avoids pretending a development branch is `main` just to obtain its tag. +if [ "${GITHUB_ACTIONS:-false}" = "true" ] && [ "${CI_PROJECT_BRANCH}" != "main" ]; then + NAMESPACE_SUFFIX="0${CI_PROJECT_SPACE}" +else + NAMESPACE_SUFFIX="" +fi export CI_PROJECT_NAMESPACE="$(dirname ${CI_PROJECT_NAME})${NAMESPACE_SUFFIX}" ; export IMG_NAMESPACE=$(echo "${CI_PROJECT_NAMESPACE}" | awk '{print tolower($0)}')