Fix deployment and load latest Codex models

This commit is contained in:
2026-07-15 20:31:23 +02:00
parent 654cd0e87c
commit 0a0f8821ce
4 changed files with 74 additions and 23 deletions

View File

@@ -15,6 +15,7 @@ Telegram-бот на Python и [kurigram](https://github.com/KurimuzonAkuma/pyro
- Linux с systemd - Linux с systemd
- Python 3.10+ - Python 3.10+
- OpenAI Codex Python SDK (`openai-codex`, ставится из `requirements.txt`) - OpenAI Codex Python SDK (`openai-codex`, ставится из `requirements.txt`)
- `curl` для установки актуального standalone Codex runtime
- Авторизация Codex: существующий Codex login или `OPENAI_API_KEY` в `.env` - Авторизация Codex: существующий Codex login или `OPENAI_API_KEY` в `.env`
Согласно официальной документации OpenAI, Python SDK устанавливается пакетом `openai-codex`, использует `AsyncCodex`, умеет `thread_start` / `thread_resume`, `turn.stream()` и поставляется с закрепленным Codex runtime. Согласно официальной документации OpenAI, Python SDK устанавливается пакетом `openai-codex`, использует `AsyncCodex`, умеет `thread_start` / `thread_resume`, `turn.stream()` и поставляется с закрепленным Codex runtime.
@@ -28,6 +29,22 @@ sudo nano /etc/codex-tg-bot/.env
sudo systemctl restart codex-tg-bot sudo systemctl restart codex-tg-bot
``` ```
`install.sh` привязывает systemd-сервис к текущему checkout репозитория. После
установки обновление кода выполняется без повторного копирования:
```bash
git pull
sudo systemctl restart codex-tg-bot
```
Если изменился `requirements.txt` или нужно обновить Codex runtime и его каталог
моделей, повторно запустите установщик:
```bash
git pull
sudo bash deploy/install.sh
```
Проверка: Проверка:
```bash ```bash

View File

@@ -1,11 +1,13 @@
"""Runs tasks through the OpenAI Codex Python SDK and yields progress events.""" """Runs tasks through the OpenAI Codex Python SDK and yields progress events."""
import asyncio import asyncio
import shutil
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any, AsyncIterator from typing import Any, AsyncIterator
from openai_codex import ApprovalMode, AsyncCodex, CodexConfig, Sandbox from openai_codex import ApprovalMode, AsyncCodex, CodexConfig, Sandbox
from openai_codex.types import ReasoningEffort, ThreadItem, ThreadTokenUsage, Turn from openai_codex.types import ThreadItem, ThreadTokenUsage, Turn
from pydantic import BaseModel, ConfigDict, Field
from . import config from . import config
@@ -73,6 +75,32 @@ class ModelOptions:
return tuple(efforts) return tuple(efforts)
class _ModelEffort(BaseModel):
model_config = ConfigDict(populate_by_name=True, extra="ignore")
reasoning_effort: str = Field(alias="reasoningEffort")
class _Model(BaseModel):
model_config = ConfigDict(populate_by_name=True, extra="ignore")
id: str
model: str
display_name: str = Field(alias="displayName")
hidden: bool = False
is_default: bool = Field(default=False, alias="isDefault")
supported_reasoning_efforts: list[_ModelEffort] = Field(
default_factory=list,
alias="supportedReasoningEfforts",
)
class _ModelListResponse(BaseModel):
model_config = ConfigDict(populate_by_name=True, extra="ignore")
data: list[_Model]
def _shorten(text: str, limit: int = TOOL_INPUT_PREVIEW_LEN) -> str: def _shorten(text: str, limit: int = TOOL_INPUT_PREVIEW_LEN) -> str:
text = " ".join(text.split()) text = " ".join(text.split())
if len(text) > limit: if len(text) > limit:
@@ -193,24 +221,15 @@ def _approval_mode() -> ApprovalMode:
) from exc ) from exc
def _effort(value: str | None) -> ReasoningEffort | None:
if value is None:
return None
try:
return ReasoningEffort(value)
except ValueError as exc:
valid = ", ".join(effort.value for effort in ReasoningEffort)
raise RuntimeError(f"Unsupported reasoning effort={value!r}; use one of: {valid}") from exc
def _codex_config() -> CodexConfig: def _codex_config() -> CodexConfig:
return CodexConfig(codex_bin=config.CODEX_BIN) return CodexConfig(codex_bin=config.CODEX_BIN or shutil.which("codex"))
def _reasoning_efforts(raw_options: Any) -> tuple[str, ...]: def _reasoning_efforts(raw_options: Any) -> tuple[str, ...]:
efforts: list[str] = [] efforts: list[str] = []
for option in raw_options or []: for option in raw_options or []:
value = getattr(getattr(option, "reasoning_effort", None), "value", None) raw_value = getattr(option, "reasoning_effort", None)
value = getattr(raw_value, "value", raw_value)
if value and value not in efforts: if value and value not in efforts:
efforts.append(value) efforts.append(value)
return tuple(efforts) return tuple(efforts)
@@ -223,7 +242,14 @@ async def list_model_options() -> ModelOptions:
if config.OPENAI_API_KEY: if config.OPENAI_API_KEY:
await codex.login_api_key(config.OPENAI_API_KEY) await codex.login_api_key(config.OPENAI_API_KEY)
response = await codex.models() # The app-server catalog evolves faster than the beta Python SDK's
# generated enums. Parse this one response permissively so newly
# introduced reasoning levels appear without an SDK release.
response = await codex._client.request(
"model/list",
{"includeHidden": False},
response_model=_ModelListResponse,
)
except Exception as exc: except Exception as exc:
return ModelOptions(models=(), error=str(exc)) return ModelOptions(models=(), error=str(exc))
@@ -318,7 +344,6 @@ async def run_task(
prompt, prompt,
approval_mode=_approval_mode(), approval_mode=_approval_mode(),
cwd=config.WORK_DIR, cwd=config.WORK_DIR,
effort=_effort(effort),
model=selected_model, model=selected_model,
sandbox=_sandbox(), sandbox=_sandbox(),
) )

View File

@@ -6,8 +6,9 @@ Wants=network-online.target
[Service] [Service]
Type=simple Type=simple
User=root User=root
WorkingDirectory=/opt/codex-tg-bot WorkingDirectory=@PROJECT_DIR@
EnvironmentFile=/etc/codex-tg-bot/.env EnvironmentFile=/etc/codex-tg-bot/.env
Environment=PATH=/opt/codex-tg-bot/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin
ExecStart=/opt/codex-tg-bot/venv/bin/python -m bot.main ExecStart=/opt/codex-tg-bot/venv/bin/python -m bot.main
Restart=on-failure Restart=on-failure
RestartSec=5 RestartSec=5

View File

@@ -15,21 +15,25 @@ if [[ $EUID -ne 0 ]]; then
fi fi
echo "==> Checking prerequisites" echo "==> Checking prerequisites"
if ! command -v python3 >/dev/null; then if ! command -v python3 >/dev/null || ! command -v curl >/dev/null; then
echo "ERROR: python3 not found. Install it, for example: apt install python3 python3-venv" >&2 echo "ERROR: python3 and curl are required" >&2
exit 1 exit 1
fi fi
echo "==> Copying project to $INSTALL_DIR" echo "==> Preparing runtime in $INSTALL_DIR"
mkdir -p "$INSTALL_DIR" mkdir -p "$INSTALL_DIR"
cp -r "$PROJECT_DIR/bot" "$PROJECT_DIR/requirements.txt" "$INSTALL_DIR/"
echo "==> Creating virtualenv and installing dependencies" echo "==> Creating virtualenv and installing dependencies"
if [[ ! -d "$INSTALL_DIR/venv" ]]; then if [[ ! -d "$INSTALL_DIR/venv" ]]; then
python3 -m venv "$INSTALL_DIR/venv" python3 -m venv "$INSTALL_DIR/venv"
fi fi
"$INSTALL_DIR/venv/bin/pip" install --upgrade pip -q "$INSTALL_DIR/venv/bin/pip" install --upgrade pip -q
"$INSTALL_DIR/venv/bin/pip" install -r "$INSTALL_DIR/requirements.txt" -q "$INSTALL_DIR/venv/bin/pip" install --upgrade --pre -r "$PROJECT_DIR/requirements.txt" -q
echo "==> Installing current standalone Codex runtime"
mkdir -p "$INSTALL_DIR/bin"
curl -fsSL https://chatgpt.com/codex/install.sh \
| CODEX_NON_INTERACTIVE=1 CODEX_INSTALL_DIR="$INSTALL_DIR/bin" sh
echo "==> Setting up config in $CONFIG_DIR" echo "==> Setting up config in $CONFIG_DIR"
mkdir -p "$CONFIG_DIR" mkdir -p "$CONFIG_DIR"
@@ -44,9 +48,13 @@ if [[ ! -f "$CONFIG_DIR/.env" ]]; then
fi fi
echo "==> Installing systemd service" echo "==> Installing systemd service"
cp "$PROJECT_DIR/deploy/$SERVICE_NAME.service" "/etc/systemd/system/$SERVICE_NAME.service" escaped_project_dir="${PROJECT_DIR//&/\\&}"
sed "s|@PROJECT_DIR@|$escaped_project_dir|g" \
"$PROJECT_DIR/deploy/$SERVICE_NAME.service" \
> "/etc/systemd/system/$SERVICE_NAME.service"
systemctl daemon-reload systemctl daemon-reload
systemctl enable --now "$SERVICE_NAME" systemctl enable "$SERVICE_NAME"
systemctl restart "$SERVICE_NAME"
echo "==> Done" echo "==> Done"
systemctl --no-pager status "$SERVICE_NAME" || true systemctl --no-pager status "$SERVICE_NAME" || true