feat(eltex): add ai-eltex-qwen, effort max by default; fix model pick and /logout-eltex

ai-eltex-qwen: new launcher for the Eltex proxy on Qwen models
(Coder-Senior, then Flash-Next), own key eltex_qwen_key, config dir and
/logout-eltex-qwen hook branch, 200K window.

Effort: ai-glm and ai-eltex start on max via CLI --effort (AI_EFFORT
overrides), CLAUDE_CODE_MAX_OUTPUT_TOKENS=64000 so long GLM answers are
not cut at 32000. EFFORT_MAPPING notes the proxy passes effort for
anth-glm-5.3 since 09.10.

ai-eltex model pick: no longer falls back to the first /v1/models entry,
which twice picked something unusable (zai-glm-5.3 routed to flash, and
the access-group name ZAI-Test-Group -> 400). Without anth-glm-5.3 the
launcher exits with a clear message; ELTEX_MODEL still overrides.

/logout-eltex: Claude Code answered "Unknown command" and never called
the logout hook (the built-in /logout bypasses hooks too). ai-eltex now
registers commands/logout-eltex.md in its config dir, so the hook
receives the command and deletes eltex_key (checked end to end).

ru-bypass: resolve exception hosts via ahostsv4 first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Виталий Никитенко
2026-10-12 11:52:45 +07:00
parent e3a1c500fd
commit 6c507b4549
9 changed files with 298 additions and 46 deletions

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env bash
# Copyright (c) 2026 Виталий Никитенко. MIT License.
# ============================================================
# AI Setup - Claude / GPT-5.5 / Grok / Qwen / DeepSeek / Kimi / GLM / Eltex / Gemini
# AI Setup - Claude / GPT-5.5 / Grok / Qwen / DeepSeek / Kimi / GLM / Eltex / Eltex Qwen / Gemini
# Запуск: bash ai-setup.sh
# ============================================================
@@ -1650,13 +1650,16 @@ export CLAUDE_CONFIG_DIR="$HOME/.config/ai-setup/cfg/glm"
# opus+sonnet+haiku = glm-5.3. Окно контекста у всех слотов задаёт env
# CLAUDE_CODE_MAX_CONTEXT_TOKENS ниже — суффикс [1m] больше не нужен.
_setup_isolated_config glm opus xhigh ''
# ai-glm стартует на /effort xhigh через CLI --effort (перебивает effortLevel из
# ai-glm стартует на /effort max через CLI --effort (перебивает effortLevel из
# settings.json при каждом запуске). Z.ai сворачивает 5 уровней Claude Code в 3
# нативных GLM 5.3: low,medium→low; high,xhigh→high; max→max (см. EFFORT_MAPPING.md).
# CLAUDE_CODE_ALWAYS_ENABLE_EFFORT заставляет Claude Code отправлять effort в API
# для GLM (иначе glm-5.3 не распознан как effort-capable и параметр не уходит,
# а Z.ai по умолчанию берёт max).
# /effort внутри сессии работает; ручной override запуска: AI_EFFORT=max ai-glm.
# /effort внутри сессии работает; ручной override запуска: AI_EFFORT=high ai-glm.
# CLAUDE_CODE_MAX_OUTPUT_TOKENS=64000: по умолчанию Claude Code просит у glm-5.3
# max_tokens=32000 (размышления + ответ), на трудных задачах GLM в них не укладывается
# и ответ обрывается. Z.ai принимает и 128000.
# CLAUDE_CODE_DISABLE_1M_CONTEXT НЕ выставляем: GLM 5.3 реально держит 1M контекста,
# а флаг заставлял бы счётчик ctx: считать окно = 200K (врал в ~5 раз). Прочие
# лаунчеры (deepseek/kimi/openrouter/opencode) флаг оставляют — у них другой контекст
@@ -1678,9 +1681,10 @@ ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME="GLM 5.3" \
ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION="GLM 5.3 — флагман Zhipu AI" \
CLAUDE_CODE_SUBAGENT_MODEL=glm-5.3 \
CLAUDE_CODE_MAX_CONTEXT_TOKENS=1000000 \
CLAUDE_CODE_MAX_OUTPUT_TOKENS=64000 \
CLAUDE_CODE_ALWAYS_ENABLE_EFFORT=1 \
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 \
claude --dangerously-skip-permissions --effort "${AI_EFFORT:-xhigh}" --system-prompt-file "$_PROMPT_FILE" "$@"
claude --dangerously-skip-permissions --effort "${AI_EFFORT:-max}" --system-prompt-file "$_PROMPT_FILE" "$@"
GLMEOF
chmod +x "$BIN_DIR/ai-glm"
@@ -1770,19 +1774,29 @@ if [ -z "$api_key" ]; then
fi
fi
# Модель: ELTEX_MODEL, иначе anth-glm-5.3, если она есть у ключа, иначе первая из
# разрешённых ключу (у других ключей другие модели, например Eltex-Coder-Senior из доки).
# anth-glm-5.3 - деплойменты на Anthropic-эндпоинте Z.ai: всегда настоящая glm-5.3 и
# картинки через серверный analyze_image. zai-glm-5.3 - OpenAI-эндпоинт с автороутером,
# который подменяет модель на glm-5.3-flash.
# Модель: ELTEX_MODEL, иначе anth-glm-5.3 - деплойменты на Anthropic-эндпоинте Z.ai:
# всегда настоящая glm-5.3 и картинки через серверный analyze_image.
# Первую модель из /v1/models НЕ берём: там бывают не те модели (zai-glm-5.3 с автороутером
# во flash) и имена групп доступа (ZAI-Test-Group), которые не запустить. Если у ключа
# нет anth-glm-5.3 - выходим с понятной ошибкой вместо 400/403 внутри Claude Code.
model="${ELTEX_MODEL:-}"
if [ -z "$model" ]; then
model=$(echo "$_CLAUDE_TEST_BODY" | python3 -c '
if avail=$(echo "$_CLAUDE_TEST_BODY" | python3 -c '
import sys, json
ids = [m["id"] for m in json.load(sys.stdin).get("data", [])]
print("anth-glm-5.3" if "anth-glm-5.3" in ids else (ids[0] if ids else ""))' 2>/dev/null)
print(" ".join(m["id"] for m in json.load(sys.stdin)["data"]))' 2>/dev/null); then
case " $avail " in
*" anth-glm-5.3 "*) model="anth-glm-5.3" ;;
*)
echo -e "\033[0;31m[МОДЕЛЬ]\033[0m Ключу недоступна модель anth-glm-5.3. Прокси отдаёт: ${avail:-(пусто)}"
echo " Напиши админам про доступ к anth-glm-5.3 или задай модель явно: ELTEX_MODEL=<имя> ai-eltex"
exit 1
;;
esac
else
# Список моделей не получили (ошибка сервера) - пробуем модель по умолчанию
model="anth-glm-5.3"
fi
fi
model="${model:-anth-glm-5.3}"
echo "Модель: $model"
_PROMPT_FILE=$(mktemp /tmp/ai-sys-prompt.XXXXXX)
@@ -1792,9 +1806,17 @@ export AI_LAUNCHER=eltex
export CLAUDE_CONFIG_DIR="$HOME/.config/ai-setup/cfg/eltex"
# Пикер: все три слота = модель ключа. availableModels НЕ задаём (он схлопывает пикер);
# незаданный слот показал бы чужой Claude.
# Effort: Claude Code шлёт output_config.effort и без CLAUDE_CODE_ALWAYS_ENABLE_EFFORT,
# но LiteLLM прокси его вырезает, поэтому /effort пока ни на что не влияет (07.10:
# напрямую в Z.ai low/max = 81/3712 выходных токенов, через прокси ~3-5K на любом уровне).
# Effort как у ai-glm: старт на max через CLI --effort (перебивает effortLevel из
# settings.json; AI_EFFORT=max в settings Claude Code не принимает и шлёт high), /effort
# в сессии работает. С 09.10 прокси пропускает output_config.effort для anth-glm-5.3:
# low/medium/high/max = 68/83/~4-6K/~4-5K выходных токенов (Z.ai: low,medium->low;
# high,xhigh->high; max). CLAUDE_CODE_ALWAYS_ENABLE_EFFORT - страховка, как в ai-glm.
# Имена-алиасы claude-* на прокси НЕ используем: claude-3-5-sonnet Claude Code считает
# старым Claude 3.5 Sonnet (без effort и thinking, max_tokens=8192); у claude-5-5-sonnet
# один деплоймент на всех, модели говорят, что она Claude Sonnet, а statusline показывает
# аккаунт Claude.ai. На качество имя не влияет (09.10, одинаковый запрос с разной строкой
# "You are powered by": разброс внутри одного имени больше разницы между именами).
# CLAUDE_CODE_MAX_OUTPUT_TOKENS=64000 вместо дефолтных 32000, как у ai-glm.
# API_TIMEOUT_MS из доки админов: прокси бывает медленным.
# Окно контекста 1M, как у ai-glm (та же glm-5.3 на Anthropic-эндпоинте Z.ai; через прокси
# 07.10 прошёл запрос на 598K входных токенов). Автокомпакт на 200K из доки админов и
@@ -1804,8 +1826,17 @@ export CLAUDE_CONFIG_DIR="$HOME/.config/ai-setup/cfg/eltex"
# ENABLE_TOOL_SEARCH НЕ включаем: на anthropic-деплойментах прокси (Z.ai /api/anthropic)
# GLM с ним путается - ищет Read/Bash через ToolSearch и решает, что их нет. Цена -
# схемы встроенных тулов в каждом запросе (~+11K входных токенов).
_setup_isolated_config eltex opus high ''
_apply_effort eltex high
_setup_isolated_config eltex opus xhigh ''
# /logout-eltex: без зарегистрированной команды Claude Code отвечает "Unknown command" и
# не зовёт UserPromptSubmit-хук (logout-account-hook.sh), а встроенный /logout хук обходит.
# Заглушка-команда в конфиге лаунчера пропускает /logout-eltex до хука, он удаляет ключ.
mkdir -p "$CLAUDE_CONFIG_DIR/commands"
cat > "$CLAUDE_CONFIG_DIR/commands/logout-eltex.md" <<'CMDEOF'
---
description: Удалить сохранённый ключ ai-eltex (обрабатывает хук, без LLM)
---
Хук не сработал: удали ~/.config/ai-setup/eltex_key вручную и перезапусти ai-eltex (или запусти AI_RESET_KEY=1 ai-eltex).
CMDEOF
ANTHROPIC_BASE_URL="$eltex_base_url" \
ANTHROPIC_AUTH_TOKEN="$api_key" \
ANTHROPIC_DEFAULT_OPUS_MODEL="$model" \
@@ -1819,12 +1850,151 @@ ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME="$model" \
ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION="$model - корпоративный прокси Eltex" \
CLAUDE_CODE_SUBAGENT_MODEL="$model" \
CLAUDE_CODE_MAX_CONTEXT_TOKENS=1000000 \
CLAUDE_CODE_MAX_OUTPUT_TOKENS=64000 \
CLAUDE_CODE_ALWAYS_ENABLE_EFFORT=1 \
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 \
API_TIMEOUT_MS=600000 \
claude --dangerously-skip-permissions --system-prompt-file "$_PROMPT_FILE" "$@"
claude --dangerously-skip-permissions --effort "${AI_EFFORT:-max}" --system-prompt-file "$_PROMPT_FILE" "$@"
ELTEXEOF
chmod +x "$BIN_DIR/ai-eltex"
# === ai-eltex-qwen ===
cat > "$BIN_DIR/ai-eltex-qwen" << 'ELTEXQWENEOF'
#!/usr/bin/env bash
# ai-eltex-qwen - запуск Claude Code через корпоративный LLM-прокси Eltex на моделях Qwen
# (Eltex-Coder-Senior = Qwen3.8-27B, Eltex-Qwen3_8-Flash-Next = Qwen3.8-Flash-Next-FP8).
# Ключ отдельный от ai-eltex, его выдают админы.
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/ai-api-helpers.sh" 2>/dev/null || true
key_file="$HOME/.config/ai-setup/eltex_qwen_key"
api_key=""
reauth=0
# Принудительный сброс сохранённого ключа через AI_RESET_KEY=1
if [ "${AI_RESET_KEY:-0}" == "1" ] && [ -f "$key_file" ]; then
rm -f "$key_file"
api_key=""
fi
[ -f "$key_file" ] && api_key=$(cat "$key_file")
# Ключ из онбординга Eltex AI (переменная для opencode), если своего файла ещё нет
if [ -z "$api_key" ] && [ "${AI_RESET_KEY:-0}" != "1" ]; then
api_key="${AI_ELTEX_LLM_API_KEY:-}"
fi
eltex_base_url="${ELTEX_BASE_URL:-http://llm.eltex.loc:4000}"
# _eltex_check_key: проверка ключа через GET /v1/models (как в ai-eltex), заодно отдаёт
# модели, разрешённые ключу.
_eltex_check_key() {
local response
# Без "|| echo 000": при сетевой ошибке -w сам печатает 000.
response=$(curl -s -w "\n%{http_code}" --max-time 15 "$eltex_base_url/v1/models" \
-H "Authorization: Bearer $1" \
2>/dev/null)
_CLAUDE_TEST_CODE=$(echo "$response" | tail -1)
_CLAUDE_TEST_BODY=$(echo "$response" | sed '$d')
if [ "$_CLAUDE_TEST_CODE" = "000" ]; then
echo ""
echo -e "\033[0;31m[СЕТЬ]\033[0m Прокси $eltex_base_url недоступен."
echo " Нужна сеть Eltex. Если не резолвится llm.eltex.loc - перезапусти ru-bypass (добавит его в /etc/hosts)"
echo " или задай адрес явно: ELTEX_BASE_URL=http://172.16.4.68:4000 ai-eltex-qwen"
exit 1
fi
_handle_openai_api_response "Eltex LLM" "$_CLAUDE_TEST_CODE" "$_CLAUDE_TEST_BODY" "Лимит исчерпан - обратись к админам"
}
if [ -n "$api_key" ]; then
echo -n "Проверка сохранённого Eltex Qwen ключа... "
_eltex_check_key "$api_key"
ret=$_API_RET
if [ $ret -eq 401 ]; then
rm -f "$key_file"
api_key=""
reauth=1
elif [ $ret -eq 429 ]; then
echo -n "Продолжить всё равно? (запросы могут не проходить) [y/N] "
read -r _ans; case "${_ans:-N}" in [Yy]*) ;; *) exit 1 ;; esac
elif [ $ret -ne 0 ]; then
exit 1
fi
fi
if [ -z "$api_key" ] && [ "$reauth" -eq 1 ]; then
echo -n "Хотите ввести новый Eltex Qwen ключ? [Y/n] "
read -r _ans; case "${_ans:-Y}" in [Yy]*) ;; *) exit 1 ;; esac
fi
if [ -z "$api_key" ]; then
echo "Ключ выдают админы (ключ для Eltex-Coder-Senior, не GLM-ключ от ai-eltex)."
echo "Можно задать его в AI_ELTEX_LLM_API_KEY, как в онбординге Eltex AI."
read -r -p "Введите ваш Eltex Qwen ключ (sk-...): " api_key
[ -z "$api_key" ] && { echo "Выход."; exit 1; }
echo -n "Проверяю ключ... "
_eltex_check_key "$api_key"
ret=$_API_RET
if [ $ret -eq 0 ] || [ $ret -eq 429 ]; then
mkdir -p "$(dirname "$key_file")"
echo "$api_key" > "$key_file"
chmod 600 "$key_file"
echo "Ключ сохранён."
if [ $ret -eq 429 ]; then
echo -n "Продолжить всё равно? (запросы могут не проходить) [y/N] "
read -r _ans; case "${_ans:-N}" in [Yy]*) ;; *) exit 1 ;; esac
fi
else
echo "Ключ НЕ сохранён."
exit 1
fi
fi
# Модель: ELTEX_QWEN_MODEL, иначе Eltex-Coder-Senior, если она есть у ключа, иначе
# Eltex-Qwen3_8-Flash-Next. Эмбеддер/реранкер из /v1/models для чата не годятся.
model="${ELTEX_QWEN_MODEL:-}"
if [ -z "$model" ]; then
model=$(echo "$_CLAUDE_TEST_BODY" | python3 -c '
import sys, json
ids = [m["id"] for m in json.load(sys.stdin).get("data", [])]
print(next((m for m in ("Eltex-Coder-Senior", "Eltex-Qwen3_8-Flash-Next") if m in ids), ""))' 2>/dev/null)
fi
model="${model:-Eltex-Coder-Senior}"
echo "Модель: $model"
_PROMPT_FILE=$(mktemp /tmp/ai-sys-prompt.XXXXXX)
trap 'rm -f "$_PROMPT_FILE"' EXIT INT TERM
_build_ai_sys_prompt > "$_PROMPT_FILE"
export AI_LAUNCHER=eltex-qwen
export CLAUDE_CONFIG_DIR="$HOME/.config/ai-setup/cfg/eltex-qwen"
# Пикер: все три слота = модель ключа (незаданный слот показал бы чужой Claude).
# Окно 200K: у Qwen3.8-Flash-Next max_input_tokens=204800. CLAUDE_CODE_DISABLE_1M_CONTEXT
# обязателен: без него Claude Code на чистом конфиге переводит opus в opus[1m] (окно 1M).
# Effort прокси игнорирует (08.10: low/high = 3227/3297, 09.10: low/max = 4424-5838/
# 977-3370 выходных токенов), /effort ни на что не влияет. Стартуем на max через CLI
# --effort, как ai-glm и ai-eltex. Картинки Qwen3.8-Flash-Next видит сама.
# max_output_tokens=8192 из /models прокси не применяет (09.10 принят max_tokens=64000),
# поэтому вывод поднят до 64000, как у ai-glm и ai-eltex.
_setup_isolated_config eltex-qwen opus high ''
ANTHROPIC_BASE_URL="$eltex_base_url" \
ANTHROPIC_AUTH_TOKEN="$api_key" \
ANTHROPIC_DEFAULT_OPUS_MODEL="$model" \
ANTHROPIC_DEFAULT_OPUS_MODEL_NAME="$model" \
ANTHROPIC_DEFAULT_OPUS_MODEL_DESCRIPTION="$model - корпоративный прокси Eltex" \
ANTHROPIC_DEFAULT_SONNET_MODEL="$model" \
ANTHROPIC_DEFAULT_SONNET_MODEL_NAME="$model" \
ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION="$model - корпоративный прокси Eltex" \
ANTHROPIC_DEFAULT_HAIKU_MODEL="$model" \
ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME="$model" \
ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION="$model - корпоративный прокси Eltex" \
CLAUDE_CODE_SUBAGENT_MODEL="$model" \
CLAUDE_CODE_DISABLE_1M_CONTEXT=1 \
CLAUDE_CODE_MAX_OUTPUT_TOKENS=64000 \
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 \
API_TIMEOUT_MS=600000 \
claude --dangerously-skip-permissions --effort "${AI_EFFORT:-max}" --system-prompt-file "$_PROMPT_FILE" "$@"
ELTEXQWENEOF
chmod +x "$BIN_DIR/ai-eltex-qwen"
# === ai-openrouter ===
cat > "$BIN_DIR/ai-openrouter" << 'OPENROUTEREOF'
#!/usr/bin/env bash
@@ -2126,7 +2296,8 @@ source "$BIN_DIR/ai-api-helpers.sh"
_setup_isolated_config deepseek opus high ''
_setup_isolated_config kimi opus high ''
_setup_isolated_config glm opus xhigh ''
_setup_isolated_config eltex opus high ''
_setup_isolated_config eltex opus xhigh ''
_setup_isolated_config eltex-qwen opus high ''
_setup_isolated_config openrouter "openai/gpt-5.5" high ''
_setup_isolated_config opencode deepseek-v4-pro high ''
@@ -2137,7 +2308,7 @@ rm -f "$BIN_DIR/claude_gpt" "$BIN_DIR/claude_deepseek" "$BIN_DIR/claude_kimi" "$
if [ "$BIN_DIR" != "$HOME/.local/bin" ]; then
warn "BIN_DIR=$BIN_DIR — удаляю старые скрипты из ~/.local/bin/ ..."
rm -f "$HOME/.local/bin/ai-gpt" "$HOME/.local/bin/ai-deepseek" "$HOME/.local/bin/ai-kimi" \
"$HOME/.local/bin/ai-glm" "$HOME/.local/bin/ai-eltex" "$HOME/.local/bin/ai-openrouter" "$HOME/.local/bin/ai-gemini" "$HOME/.local/bin/ai-cursor" "$HOME/.local/bin/ai-claude" \
"$HOME/.local/bin/ai-glm" "$HOME/.local/bin/ai-eltex" "$HOME/.local/bin/ai-eltex-qwen" "$HOME/.local/bin/ai-openrouter" "$HOME/.local/bin/ai-gemini" "$HOME/.local/bin/ai-cursor" "$HOME/.local/bin/ai-claude" \
"$HOME/.local/bin/ai-opencode" \
"$HOME/.local/bin/ai-api-helpers.sh" "$HOME/.local/bin/claude-gpt-effort-proxy.py"
fi
@@ -2158,6 +2329,7 @@ echo -e " ${CYAN}ai-deepseek${NC} - DeepSeek (через Claude Code,
echo -e " ${CYAN}ai-kimi${NC} - Kimi K2.7 Code (через Claude Code, API ключ сохраняется)"
echo -e " ${CYAN}ai-glm${NC} - GLM 5.3 (через Claude Code, Zhipu AI API)"
echo -e " ${CYAN}ai-eltex${NC} - Корпоративный LLM-прокси Eltex (через Claude Code, ключ от админов)"
echo -e " ${CYAN}ai-eltex-qwen${NC} - Корпоративный LLM-прокси Eltex, модели Qwen (через Claude Code, свой ключ)"
echo -e " ${CYAN}ai-openrouter${NC} - OpenRouter (через Claude Code: GPT-5.5, Grok, Qwen, GLM 5.2 и др.)"
echo -e " ${CYAN}ai-opencode${NC} - OpenCode Go (через Claude Code + routatic-proxy: DeepSeek, Kimi, Qwen, GLM, MiniMax, MiMo)"
echo ""

View File

@@ -245,7 +245,8 @@ if [ -n "${ALL_EXC// }" ]; then
if echo "$item" | grep -qE "^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$"; then
ips="$item"
else
ips=$(getent hosts "$item" 2>/dev/null | awk '{print $1}' | sort -u)
ips=$(getent ahostsv4 "$item" 2>/dev/null | awk '{print $1}' | sort -u)
[ -z "$ips" ] && ips=$(getent hosts "$item" 2>/dev/null | awk '{print $1}' | sort -u)
fi
for ip in $ips; do
ipset add ru-direct "$ip" -exist 2>/dev/null || true
@@ -299,7 +300,8 @@ if [ -n "${ALL_EXC// }" ]; then
if echo "$item" | grep -qE "^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$"; then
ips="$item"
else
ips=$(getent hosts "$item" 2>/dev/null | awk '{print $1}' | sort -u)
ips=$(getent ahostsv4 "$item" 2>/dev/null | awk '{print $1}' | sort -u)
[ -z "$ips" ] && ips=$(getent hosts "$item" 2>/dev/null | awk '{print $1}' | sort -u)
fi
for ip in $ips; do
ip route replace "$ip/32" via "$GATEWAY" dev "$DEV" 2>/dev/null