Sign inSign up

wenchiehlee1020/llm-cli-api-server

By wenchiehlee1020

•Updated 5 days ago

LLM CLI & MCP Bridge Server on Synology NAS (Codex CLI, Antigravity CLI, OpenAI Tunnel)

Image
Machine learning & AI
0

1.4K

wenchiehlee1020/llm-cli-api-server repository overview

⁠LLM CLI API Server (Synology NAS Deployment)

Docker Pulls Docker Image Version Docker Image Size

本專案旨在 Synology NAS 上部署一個具備 Python 環境與 OpenAI codex-cli 的 Docker 容器,並透過 GitHub Actions 實現自動化部署。

⁠API Usage Statistics

📊 用量統計已整合到 llm 函式庫 README⁠(Llm-Cli-APIServer 段落),不再於本檔案獨立維護一份會漂移的副本。

⁠🚀 快速開始

⁠1. 準備環境

在您的 Synology NAS 上,確保已安裝 Container Manager (Docker) 並運行了 GitHub Self-hosted Runner。

⁠2. 設定 GitHub Secrets

進入 GitHub Repository 的 Settings > Secrets and variables > Actions,新增以下 Secret:

  • SSH_ROOT_PASSWORD: 容器 SSH root 密碼(必填)。
  • CODEX_API_KEY: 保護 /codex/exec 端點的存取金鑰(選填,留空則不驗證)。
⁠3. 部署專案

將代碼 Push 到 main 分支後,GitHub Actions 會自動同步程式碼並重新建立容器。第一次部署會自動 build image;之後一般部署會沿用既有 Docker image,不重跑 build。只有 Dockerfile、系統套件、Codex CLI、agy 或 Python dependency 變更時,手動執行 workflow 並設定 rebuild_image=true。

⁠4. 🔑 第一次授權與遠端配對

由於 codex-cli 需要瀏覽器登入,請依照以下步驟完成一次性授權:

  1. 開啟 GitHub Repository 的 Actions 分頁。
  2. 查看部署任務日誌。
  3. 若尚未登入,workflow 會執行 codex login,日誌中會出現瀏覽器授權連結與一次性代碼。
  4. 在您的電腦瀏覽器開啟該連結,並輸入代碼以完成授權。
  5. 授權成功後,認證資訊會持久化在 NAS 的 /volume1/docker/llm-cli-api-server/codex-auth,並保留 /volume1/docker/llm-cli-api-server/config 舊路徑相容。
  6. 容器會在背景維持 codex remote-control app-server;workflow 會輸出 codex remote-control pair 的短效配對碼,供本機 Codex 或 iOS app 遠端配對使用。
  7. 從此以後,所有的自動部署都將維持登入狀態,無需再次授權。
⁠5. 🔑 agy(Antigravity CLI)授權

/gemini/exec、/smart/exec(gemini CLI)的主要路徑已改用 agy,取代已於 2026-06-18 停用的 gemini-cli OAuth 登入。與 Codex 不同,agy 的登入是直接在 NAS container 內完成,不是在筆電登入後再複製 token:

  1. 執行 skills/skill-llm-api-server/scripts/renew-agy-auth.sh(或手動 ssh 進 NAS 後 docker exec -it llm-cli-api-server agy)。
  2. 跟著畫面印出的網址,在自己的瀏覽器完成 Google 帳號登入。
  3. 登入完成後,OAuth token 會寫入 container 內的持久化路徑(掛載於 ./gemini-auth),container 重啟後仍會保留。
  4. 可用 curl http://llm-cli-api.tail28f10.ts.net:5001/gemini/status 驗證 gemini_cli 是否為 installed。
⁠6. 🔑 OpenAI Secure MCP Tunnel(ChatGPT 連線)

本專案整合了 local-workspace-mcp 與 OpenAI 官方 tunnel-client,讓一般 ChatGPT(網頁版或 App)可以直接透過 Secure MCP Tunnel 存取 NAS 容器環境與檔案:

  1. 取得通道 ID 與金鑰:
  2. 部署設定:
    • 在 GitHub Repository 的 Settings > Secrets and variables > Actions 中新增 Secret OPENAI_TUNNEL_RUNTIME_KEY 與 Variable/Secret OPENAI_TUNNEL_ID。
    • 因為新增了 tunnel-client 與 MCP 套件,第一次啟用請在 GitHub Actions 執行 Deploy Llm-Cli-APIServer to NAS 並設定 rebuild_image=true。
  3. 自動啟動:
    • 容器啟動時會透過 scripts/start-openai-tunnel.sh 在背景常駐維護 tunnel-client,並自動重連。
    • 可用 GET /tunnel/status 檢查通道是否正常運作。
  4. ChatGPT 連線使用:
    • 開啟 ChatGPT 網頁端或桌面版「設定 > 安全性與登入 > 開發者模式」。
    • 在「外掛程式/應用程式」選擇新增通道,輸入您的 Tunnel ID,驗證方式選擇「無驗證(No authentication)」。
    • 建立完成後,於任何普通 ChatGPT 對話中即可直接使用本機檔案、終端指令及 NAS 工具。

⁠🛠 技術架構

  • 基礎環境: Python 3.11-slim + Node.js 20
  • 工具: OpenAI codex-cli (@openai/codex)
  • API 框架: Flask (容器內 Port 5001,對外 Port 5055)
  • SSH 存取: 容器內建獨立 tailscaled,Tailscale hostname 為 llm-cli-api,可直連 llm-cli-api.tail28f10.ts.net:22,不需透過 NAS 主機轉發
  • 部署模式: Docker image 只提供 OS/CLI/dependency;NAS 部署目錄 bind mount 到 /app,所以一般程式碼更新只需 rsync + recreate,不需重建 image。
  • 持久化:
    • /app -> NAS /volume1/docker/llm-cli-api-server (rsync 後的目前程式碼)
    • /app/data -> NAS data/ (存放應用程式資料)
    • /root/.codex -> NAS codex-auth/ (Codex 目前授權與 remote-control 狀態)
    • /root/.config/codex -> NAS config/ (Codex 舊授權路徑相容)

⁠📡 伺服器狀態

Updated: 2026-11-07 15:14 CST

⁠網路連通性
存取方式位址狀態
外網 HTTPS (WAN)http://llm-cli-api.tail28f10.ts.net:5001❌ 無法連線 (Runner 限制)
容器網關 (Docker)http://172.18.0.1:5055✅ 正常
⁠元件狀態
組件端點狀態
Gemini CLI/gemini/status✅ 已安裝 (1.5.1)
Codex CLI/codex/status✅ 已安裝 (codex-cli 0.167.0)

⁠📡 API 測試

外網統一使用 http://llm-cli-api.tail28f10.ts.net:5001;內網可使用 http://newton.tail28f10.ts.net:5055(透過 NAS 主機轉發)或 http://llm-cli-api.tail28f10.ts.net:5001(容器 Tailscale 直連,不需 NAS 防火牆放行)。

⁠504 Gateway Timeout 診斷

Prompt 字元限制可用 CODEX_MAX_PROMPT_LENGTH 與 GEMINI_MAX_PROMPT_LENGTH 設定;預設 0 表示 server 不設字元 guard,但 CLI、gateway 與模型本身仍可能有 context window 限制。若設定正數,超限請求會回傳 HTTP 413。

/exec 會以 CODEX_TIMEOUT 限制 codex exec 的最長執行時間,預設為 120 秒。若 Codex CLI 超過此時間仍未回應,API server 會回傳 HTTP 504,response body 會包含:

{
  "error": "codex timed out after 120s"
}

若 client 端看到約 120 秒後失敗,優先檢查 container log:

docker logs llm-cli-api-server --since "2026-05-12T11:43:00Z"

相關 log 範例:

CLI start: cli=codex timeout=120s prompt_chars=...
CLI timeout: cli=codex elapsed=120.0s timeout=120s

若 container log 有 CLI timeout,代表 timeout 發生在 API server 內的 Codex CLI 執行流程;若沒有對應 log,才優先檢查 Synology reverse proxy、nginx、Cloudflare 等外層 gateway timeout。可在 GitHub repository variables 設定 CODEX_TIMEOUT 與 GEMINI_TIMEOUT,deploy workflow 會寫入 NAS 的 .env。

⁠Codex apply_patch / bubblewrap 診斷

若在 container 內使用 Codex CLI 時看到:

bwrap: Creating new namespace failed, likely because the kernel does not support user namespaces. bwrap must be installed setuid on such systems.

這通常不是 Flask API 程式錯誤,而是 Codex sandbox 依賴的 bubblewrap 無法建立 Linux namespace。此 image 已安裝 bubblewrap 並在 build 階段執行 chmod u+s /usr/bin/bwrap;container 啟動時也會執行一次 sandbox smoke test,失敗時會在 log 中輸出 bubblewrap sandbox check failed。

可在 host/container 檢查:

bwrap --version
ls -l "$(command -v bwrap)"
sysctl kernel.unprivileged_userns_clone
sysctl user.max_user_namespaces

常見修法:

sudo sysctl -w kernel.unprivileged_userns_clone=1
sudo sysctl -w user.max_user_namespaces=15000
sudo chown root:root "$(command -v bwrap)"
sudo chmod u+s "$(command -v bwrap)"

若 host kernel 不提供這些 sysctl,或容器平台禁止 user namespace,即使 container 使用 privileged: true 仍可能失敗;這時要改 host/container runtime 設定,或讓 Codex 使用不依賴 bubblewrap 的執行模式。

⁠Codex remote-control 配對

容器啟動時預設會由 /app/scripts/start-codex-remote-control.sh 在背景維持 foreground codex remote-control app-server,行為等同 SSH 進容器後手動執行 codex remote-control。部署 workflow 完成授權檢查後,會輸出 codex remote-control pair 的短效配對碼;若暫時不需要遠端配對,可在 GitHub repository variables 設定 CODEX_REMOTE_CONTROL=0 停用。若 app-server 因授權尚未完成或網路暫時失敗而結束,supervisor 會依 CODEX_REMOTE_CONTROL_RESTART_DELAY 秒數重試,預設 10 秒。容器內可用 tail -f /tmp/codex_remote_control.log 檢查狀態。

⁠基本端點
  • 健康檢查: GET /
    • 回應: {"status": "ready", "service": "LLM CLI API Server"}
  • Codex CLI 狀態: GET /codex/status
    • 回應: {"codex_cli": "installed", "version": "codex-cli 0.128.0"}(未安裝時回傳 503)
  • Gemini CLI 狀態: GET /gemini/status(已改為 agy,見上方第 5 節)
    • 回應: {"gemini_cli": "installed", "tool": "agy", "auth_mode": "oauth", "version": "agy 1.1.22"}
⁠Codex 執行端點

需在 Header 帶 X-API-Key(若未設定 CODEX_API_KEY 則不需要)。

此端點與 llm 函式庫的 CodexProvider 相容,設定方式:

CODEX_API_KEY=your-key

curl 範例:

curl -X POST http://llm-cli-api.tail28f10.ts.net:5001/exec \
  -H "Content-Type: application/json" \
  -H "X-API-Key: your-key" \
  -d '{"prompt": "請幫我寫一個 Python 的 hello world 程式"}'

Request 選填欄位:

{
  "prompt": "...",
  "model": "gpt-5-codex"
}

不帶 model 時,沿用 codex CLI 自己的預設 model(由該容器的 Codex 登入帳號/plan 決定,可用 codex model --help 或 /codex/help 查詢實際可用值);帶了 model 才會在指令加上 --model <model>。

回應格式:

{
  "output": "print('Hello, World!')"
}
⁠Gemini 執行端點

需在 Header 帶 X-API-Key(若未設定 CODEX_API_KEY 則不需要)。

curl 範例:

curl -X POST http://llm-cli-api.tail28f10.ts.net:5001/gemini/exec \
  -H "Content-Type: application/json" \
  -H "X-API-Key: your-key" \
  -d '{"prompt": "請幫我寫一個 Python 的 hello world 程式", "model": "gemini-2.5-flash"}'

Request 選填欄位:

{
  "prompt": "...",
  "model": "gemini-2.5-flash",
  "json_mode": false
}

回應格式:

{
  "output": "print('Hello, World!')"
}

Python smoke test:

# 內網透過 NAS 主機轉發
python check_gemini_cli.py

# 內網容器 Tailscale 直連(推薦,不受 NAS 防火牆影響)
python check_gemini_cli.py

# 外網或需要 API key 時
CODEX_API_KEY=your-key \
GEMINI_MODEL=gemini-2.5-flash \
python check_gemini_cli.py

此腳本會依序呼叫 GET /gemini/status 與 POST /gemini/exec,可用來確認 Gemini CLI 是否已安裝、授權且可實際產生回應。

⁠Smart Routing 錯誤診斷

POST /smart/exec 會先執行 draft provider,再由 judge provider 判斷 draft 是否可接受。若 draft 或 judge 階段發生錯誤,API 會回傳結構化診斷欄位,方便判斷 fallback 的 root cause。

錯誤回應範例:

{
  "smart_status": "error",
  "fallback_reason": "auth_failure",
  "failed_stage": "draft",
  "provider": "gemini",
  "error": "gemini exited 1: 401 Unauthorized"
}

診斷欄位:

欄位說明
smart_statusSmart Routing 狀態;錯誤時為 error。
fallback_reason錯誤分類,例如 timeout、auth_failure、cli_not_found、nonzero_exit、unknown_error。
failed_stage失敗發生階段:draft 或 judge。
provider失敗階段使用的 provider,例如 gemini 或 codex。
error原始錯誤訊息,供 log 或人工追查使用。

fallback reason 分類:

Reason常見原因
timeoutCLI 執行超過 CODEX_TIMEOUT 或 GEMINI_TIMEOUT。
auth_failureCLI token 過期、未登入、401 Unauthorized、credential 問題。
cli_not_foundcontainer 內找不到 codex 或 gemini command。
nonzero_exitCLI 有執行但 return code 非 0,且錯誤不像 auth 問題。
unknown_error其他未分類例外。

有了這些欄位後,README 的 Smart Routing 統計不應只看 Fallback (Error) 總數;應進一步依 fallback_reason、failed_stage、provider 分組,才能知道主要 root cause 是授權、timeout、CLI 安裝,還是 provider/model 設定問題。

⁠📂 目錄結構

  • Dockerfile: 混合環境定義
  • entrypoint.sh: 啟動與認證注入腳本
  • docker-compose.yml: 容器編排與磁碟映射
  • main.py: Flask API Server 主程式
  • .github/workflows/deploy-synology-nas.yml: 自動化部署流程

Tag summary

Content type

Image

Digest

sha256:7ba6390c9…

Size

1.2 GB

Last updated

5 days ago

docker pull wenchiehlee1020/llm-cli-api-server