Fix deployment and load latest Codex models
This commit is contained in:
17
README.md
17
README.md
@@ -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
|
||||||
|
|||||||
@@ -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(),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user