feat: полная изоляция моделей между ai-* и гибридный persistence effort
Раньше все ai-* лаунчеры делили один ~/.claude и общий settings.json, из-за чего кастомная модель (openai/gpt-5.5) из ai-openrouter протекала в пикер ai-claude. Теперь каждый сторонний провайдер изолирован в своём CLAUDE_CONFIG_DIR (~/.config/ai-setup/cfg/<launcher>) - свои settings.json и .claude.json, ноль протечек. ai-claude остаётся на ~/.claude (нативный логин). Пикеры /model приведены к требуемому виду: - ai-deepseek: только DeepSeek V4 Pro (opus) и DeepSeek V4 Flash (haiku), дефолт Pro; через availableModels + ANTHROPIC_DEFAULT_*_MODEL_NAME - ai-kimi: только Kimi K2.6 (opus) - ai-claude: только нативные модели Claude Общие skills и CLAUDE.md шарятся симлинком из ~/.claude. Persistence effort - гибрид: - low/medium/high/xhigh живут нативно в settings.json лаунчера, /effort внутри сессии работает свободно и уровень сохраняется - max нельзя сохранить в settings.json (session-only), поэтому он восстанавливается через CLAUDE_CODE_EFFORT_LEVEL; в такой max-сессии /effort залочен (ограничение Claude Code), выход - AI_EFFORT=<lvl> ai-* Текущий уровень ловит статусбар в ~/.cache/ai-setup/effort_<launcher>. Удалён устаревший effort-save-hook (заменён нативным persistence + гибридом), почищен из ~/.claude/settings.json и осиротевший кэш model_*. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -683,39 +683,39 @@ else
|
||||
warn "Файл $STATUSLINE_SRC не найден, пропускаю"
|
||||
fi
|
||||
|
||||
# ── 6.7.0. Хук effort-save (сохраняет effort при завершении сессии) ──
|
||||
info "Деплою хук effort-save..."
|
||||
EFFORT_HOOK_SRC="$SCRIPT_DIR/home-configs/claude/hooks/effort-save-hook.sh"
|
||||
EFFORT_HOOK_DST="$HOME/.claude/hooks/effort-save-hook.sh"
|
||||
mkdir -p "$HOME/.claude/hooks"
|
||||
if [ -f "$EFFORT_HOOK_SRC" ]; then
|
||||
cp "$EFFORT_HOOK_SRC" "$EFFORT_HOOK_DST"
|
||||
chmod +x "$EFFORT_HOOK_DST"
|
||||
python3 - "$HOME/.claude/settings.json" "$EFFORT_HOOK_DST" <<'PYEOF'
|
||||
# ── 6.7.0. Чистка устаревшего хука effort-save ──────────────
|
||||
# effort/model теперь персистятся нативно в settings.json каждого CLAUDE_CONFIG_DIR
|
||||
# (полная изоляция лаунчеров), самопальный кэш ~/.cache/ai-setup/{effort,model}_* не нужен.
|
||||
# Удаляем старый хук с диска и из ~/.claude/settings.json (нотифаер в Stop сохраняем).
|
||||
info "Удаляю устаревший хук effort-save..."
|
||||
rm -f "$HOME/.claude/hooks/effort-save-hook.sh"
|
||||
# Осиротевший кэш моделей (источник старой протечки между ai-*); effort_* НЕ трогаем -
|
||||
# он снова используется для персиста effort (включая max) через CLAUDE_CODE_EFFORT_LEVEL.
|
||||
rm -f "$HOME"/.cache/ai-setup/model_*
|
||||
python3 - "$HOME/.claude/settings.json" <<'PYEOF'
|
||||
import sys, json, os
|
||||
settings_path, hook_path = sys.argv[1], sys.argv[2]
|
||||
data = {}
|
||||
if os.path.exists(settings_path):
|
||||
settings_path = sys.argv[1]
|
||||
if not os.path.exists(settings_path):
|
||||
sys.exit(0)
|
||||
try:
|
||||
with open(settings_path) as f:
|
||||
try: data = json.load(f)
|
||||
except json.JSONDecodeError: pass
|
||||
data.setdefault("hooks", {}).setdefault("Stop", [{"hooks": []}])
|
||||
hook_cmd = f'bash "{hook_path}"'
|
||||
stop_hooks = data["hooks"]["Stop"]
|
||||
already = any(
|
||||
any(h.get("command", "") == hook_cmd for h in entry.get("hooks", []))
|
||||
for entry in stop_hooks
|
||||
)
|
||||
if not already:
|
||||
stop_hooks[0]["hooks"].append({"type": "command", "command": hook_cmd})
|
||||
with open(settings_path, "w") as f:
|
||||
json.dump(data, f, indent=2, ensure_ascii=False)
|
||||
f.write("\n")
|
||||
data = json.load(f)
|
||||
except Exception:
|
||||
sys.exit(0)
|
||||
stop = data.get("hooks", {}).get("Stop")
|
||||
if isinstance(stop, list):
|
||||
for entry in stop:
|
||||
hooks = entry.get("hooks")
|
||||
if isinstance(hooks, list):
|
||||
entry["hooks"] = [h for h in hooks if "effort-save-hook" not in h.get("command", "")]
|
||||
data["hooks"]["Stop"] = [e for e in stop if e.get("hooks")]
|
||||
if not data["hooks"]["Stop"]:
|
||||
del data["hooks"]["Stop"]
|
||||
with open(settings_path, "w") as f:
|
||||
json.dump(data, f, indent=2, ensure_ascii=False)
|
||||
f.write("\n")
|
||||
PYEOF
|
||||
success "Хук effort-save установлен"
|
||||
else
|
||||
warn "Файл $EFFORT_HOOK_SRC не найден, пропускаю"
|
||||
fi
|
||||
success "Старый хук effort-save удалён"
|
||||
|
||||
# ── 6.7.1. Хук switch-account ───────────────────────────────────
|
||||
info "Деплою хук switch-account..."
|
||||
@@ -1077,71 +1077,95 @@ _open_browser() {
|
||||
else echo "Откройте вручную: $url"; fi
|
||||
}
|
||||
|
||||
# _restore_effort: читает сохранённый effort для текущего AI_LAUNCHER из кэша
|
||||
# и записывает его в settings.json, чтобы Claude Code подхватил нужный уровень.
|
||||
# Не передаём --effort через CLI, чтобы /effort внутри сессии работал без блокировки.
|
||||
_restore_effort() {
|
||||
local default_effort="${1:-high}"
|
||||
local launcher="${AI_LAUNCHER:-}"
|
||||
[ -z "$launcher" ] && return
|
||||
local effort_file="$HOME/.cache/ai-setup/effort_${launcher}"
|
||||
local effort
|
||||
effort=$(cat "$effort_file" 2>/dev/null)
|
||||
[ -z "$effort" ] && effort="$default_effort"
|
||||
mkdir -p "$HOME/.cache/ai-setup"
|
||||
python3 - "$HOME/.claude/settings.json" "$effort" <<'PYEOF'
|
||||
# _setup_isolated_config: готовит изолированную папку CLAUDE_CONFIG_DIR для лаунчера.
|
||||
# Каждый сторонний провайдер получает собственные settings.json и .claude.json,
|
||||
# поэтому выбор модели и кэш кастом-моделей НЕ протекают между ai-* лаунчерами.
|
||||
# Общие ресурсы (skills, CLAUDE.md) шарятся симлинком из ~/.claude.
|
||||
# model и effortLevel сидируются как дефолты - выбор юзера через /model и /effort
|
||||
# (для low/medium/high/xhigh) сохраняется нативно в этом же settings.json.
|
||||
# Уровень max обрабатывается отдельно в _apply_effort (settings.json его не хранит).
|
||||
# Использование: _setup_isolated_config <launcher> <default_model> <default_effort> <available_models_json>
|
||||
_setup_isolated_config() {
|
||||
local launcher="$1" default_model="$2" default_effort="${3:-high}" avail="${4:-}"
|
||||
local cfg="$HOME/.config/ai-setup/cfg/$launcher"
|
||||
mkdir -p "$cfg"
|
||||
# Общие ресурсы из ~/.claude (единый источник правды)
|
||||
ln -sfn "$HOME/.claude/skills" "$cfg/skills"
|
||||
ln -sfn "$HOME/.claude/CLAUDE.md" "$cfg/CLAUDE.md"
|
||||
[ -e "$HOME/.claude/agents" ] && ln -sfn "$HOME/.claude/agents" "$cfg/agents"
|
||||
# .claude.json в свежей папке: пропускаем онбординг
|
||||
[ -f "$cfg/.claude.json" ] || echo '{"hasCompletedOnboarding": true}' > "$cfg/.claude.json"
|
||||
python3 - "$cfg/settings.json" "$HOME/.claude/statusline-command.sh" \
|
||||
"$default_model" "$default_effort" "$avail" <<'PYEOF'
|
||||
import sys, json, os
|
||||
settings_path, effort = sys.argv[1], sys.argv[2]
|
||||
cfg_settings, statusline, model, effort, avail = sys.argv[1:6]
|
||||
data = {}
|
||||
if os.path.exists(settings_path):
|
||||
if os.path.exists(cfg_settings):
|
||||
try:
|
||||
with open(settings_path) as f:
|
||||
with open(cfg_settings) as f:
|
||||
data = json.load(f)
|
||||
except Exception:
|
||||
pass
|
||||
data['effortLevel'] = effort
|
||||
with open(settings_path, 'w') as f:
|
||||
# Структурные настройки лаунчера (переустанавливаем всегда)
|
||||
data["statusLine"] = {"type": "command", "command": f"bash {statusline}"}
|
||||
data["skipDangerousModePermissionPrompt"] = True
|
||||
data.setdefault("hooks", {})["SessionStart"] = [{"hooks": [{"type": "command", "command": "true"}]}]
|
||||
# availableModels - белый список пикера (политика лаунчера)
|
||||
if avail:
|
||||
data["availableModels"] = json.loads(avail)
|
||||
else:
|
||||
data.pop("availableModels", None)
|
||||
# model/effortLevel - сидируем дефолты, не перетирая выбор юзера (нативный persistence)
|
||||
if model:
|
||||
data.setdefault("model", model)
|
||||
data.setdefault("effortLevel", effort)
|
||||
with open(cfg_settings, "w") as f:
|
||||
json.dump(data, f, indent=2, ensure_ascii=False)
|
||||
f.write('\n')
|
||||
f.write("\n")
|
||||
PYEOF
|
||||
}
|
||||
|
||||
_restore_model() {
|
||||
local default_model="${1:-}"
|
||||
local launcher="${AI_LAUNCHER:-}"
|
||||
[ -z "$launcher" ] && return
|
||||
local model_file="$HOME/.cache/ai-setup/model_${launcher}"
|
||||
local model
|
||||
model=$(cat "$model_file" 2>/dev/null)
|
||||
[ -z "$model" ] && model="$default_model"
|
||||
[ -z "$model" ] && return
|
||||
python3 - "$HOME/.claude/settings.json" "$model" <<'PYEOF'
|
||||
# _apply_effort: per-launcher persistence уровня effort (гибрид).
|
||||
# - low/medium/high/xhigh живут нативно в settings.json лаунчера -> /effort работает,
|
||||
# уровень сохраняется между сессиями, env-переменная НЕ ставится.
|
||||
# - max единственный нельзя сохранить в settings.json (он session-only), поэтому
|
||||
# его восстанавливаем через CLAUDE_CODE_EFFORT_LEVEL. В такой max-сессии /effort
|
||||
# залочен env-переменной (ограничение Claude Code).
|
||||
# Текущий уровень (вкл. max) ловит статусбар в ~/.cache/ai-setup/effort_<launcher>.
|
||||
# Сменить уровень из max: AI_EFFORT=<lvl> ai-<launcher>.
|
||||
# Использование: _apply_effort <launcher> <default_effort>
|
||||
_apply_effort() {
|
||||
local launcher="$1" default_effort="${2:-high}"
|
||||
local f="$HOME/.cache/ai-setup/effort_${launcher}"
|
||||
local settings="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/settings.json"
|
||||
local eff
|
||||
if [ -n "${AI_EFFORT:-}" ]; then
|
||||
# Явный override: запоминаем и применяем
|
||||
eff="$AI_EFFORT"
|
||||
mkdir -p "$HOME/.cache/ai-setup"
|
||||
echo "$eff" > "$f"
|
||||
else
|
||||
eff=$(cat "$f" 2>/dev/null)
|
||||
fi
|
||||
if [ "$eff" = "max" ]; then
|
||||
# Единственный способ восстановить max между сессиями
|
||||
export CLAUDE_CODE_EFFORT_LEVEL=max
|
||||
elif [ -n "${AI_EFFORT:-}" ] && [ -n "$eff" ]; then
|
||||
# Явный сброс на low/medium/high/xhigh - пишем нативно в settings.json лаунчера
|
||||
python3 - "$settings" "$eff" <<'PYEOF'
|
||||
import sys, json, os
|
||||
settings_path, model = sys.argv[1], sys.argv[2]
|
||||
data = {}
|
||||
if os.path.exists(settings_path):
|
||||
try:
|
||||
with open(settings_path) as f:
|
||||
data = json.load(f)
|
||||
except Exception:
|
||||
pass
|
||||
data['model'] = model
|
||||
with open(settings_path, 'w') as f:
|
||||
json.dump(data, f, indent=2, ensure_ascii=False)
|
||||
f.write('\n')
|
||||
p, eff = sys.argv[1], sys.argv[2]
|
||||
d = {}
|
||||
if os.path.exists(p):
|
||||
try: d = json.load(open(p))
|
||||
except Exception: pass
|
||||
d["effortLevel"] = eff
|
||||
os.makedirs(os.path.dirname(p), exist_ok=True)
|
||||
with open(p, "w") as fp:
|
||||
json.dump(d, fp, indent=2, ensure_ascii=False); fp.write("\n")
|
||||
PYEOF
|
||||
}
|
||||
|
||||
# _restore_model_str: возвращает сохранённую модель строкой (для ANTHROPIC_MODEL env var)
|
||||
_restore_model_str() {
|
||||
local default_model="${1:-}"
|
||||
local launcher="${AI_LAUNCHER:-}"
|
||||
[ -z "$launcher" ] && echo "$default_model" && return
|
||||
local model_file="$HOME/.cache/ai-setup/model_${launcher}"
|
||||
local model
|
||||
model=$(cat "$model_file" 2>/dev/null)
|
||||
[ -z "$model" ] && model="$default_model"
|
||||
echo "$model"
|
||||
fi
|
||||
# Иначе (≤xhigh без AI_EFFORT): ничего не делаем - effortLevel уже персистнут нативно.
|
||||
}
|
||||
|
||||
_build_ai_sys_prompt() {
|
||||
@@ -1266,13 +1290,19 @@ _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=deepseek
|
||||
_restore_effort high
|
||||
export CLAUDE_CONFIG_DIR="$HOME/.config/ai-setup/cfg/deepseek"
|
||||
# Пикер: только DeepSeek V4 Pro (opus) и DeepSeek V4 Flash (haiku), дефолт - Pro
|
||||
_setup_isolated_config deepseek opus high '["opus", "haiku"]'
|
||||
_apply_effort deepseek high
|
||||
ANTHROPIC_BASE_URL=https://api.deepseek.com/anthropic \
|
||||
ANTHROPIC_AUTH_TOKEN="$api_key" \
|
||||
ANTHROPIC_MODEL=deepseek-v4-pro \
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL=deepseek-v4-pro \
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL_NAME="DeepSeek V4 Pro" \
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL_DESCRIPTION="DeepSeek V4 Pro - флагман для сложных задач" \
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL=deepseek-v4-pro \
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL=deepseek-v4-flash \
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME="DeepSeek V4 Flash" \
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION="DeepSeek V4 Flash - быстрый и дешёвый" \
|
||||
CLAUDE_CODE_SUBAGENT_MODEL=deepseek-v4-flash \
|
||||
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 \
|
||||
claude --dangerously-skip-permissions --system-prompt-file "$_PROMPT_FILE" "$@"
|
||||
@@ -1340,11 +1370,15 @@ _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=kimi
|
||||
_restore_effort high
|
||||
export CLAUDE_CONFIG_DIR="$HOME/.config/ai-setup/cfg/kimi"
|
||||
# Пикер: единственная модель Kimi K2.6 (под алиасом opus)
|
||||
_setup_isolated_config kimi opus high '["opus"]'
|
||||
_apply_effort kimi high
|
||||
ANTHROPIC_BASE_URL=https://api.kimi.com/coding \
|
||||
ANTHROPIC_AUTH_TOKEN="$api_key" \
|
||||
ANTHROPIC_MODEL=kimi-k2.6 \
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL=kimi-k2.6 \
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL_NAME="Kimi K2.6" \
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL_DESCRIPTION="Kimi K2.6 (Moonshot AI)" \
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL=kimi-k2.6 \
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL=kimi-k2.6 \
|
||||
CLAUDE_CODE_SUBAGENT_MODEL=kimi-k2.6 \
|
||||
@@ -1414,11 +1448,16 @@ _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=openrouter
|
||||
_restore_effort high
|
||||
_MODEL=$(_restore_model_str "openai/gpt-5.5")
|
||||
export CLAUDE_CONFIG_DIR="$HOME/.config/ai-setup/cfg/openrouter"
|
||||
# openrouter - гибкий лаунчер: пикер не ограничиваем (availableModels пустой),
|
||||
# gpt-5.5 добавляем отдельным пунктом и делаем дефолтом
|
||||
export ANTHROPIC_CUSTOM_MODEL_OPTION="openai/gpt-5.5"
|
||||
export ANTHROPIC_CUSTOM_MODEL_OPTION_NAME="GPT-5.5"
|
||||
export ANTHROPIC_CUSTOM_MODEL_OPTION_DESCRIPTION="OpenRouter: openai/gpt-5.5"
|
||||
_setup_isolated_config openrouter "openai/gpt-5.5" high ''
|
||||
_apply_effort openrouter high
|
||||
ANTHROPIC_BASE_URL=https://openrouter.ai/api \
|
||||
ANTHROPIC_AUTH_TOKEN="$api_key" \
|
||||
ANTHROPIC_MODEL=$_MODEL \
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL=anthropic/claude-4.8-opus \
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL=anthropic/claude-4.6-sonnet \
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL=openai/gpt-5.5 \
|
||||
@@ -1469,8 +1508,11 @@ _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=claude
|
||||
_restore_effort xhigh
|
||||
_restore_model "sonnet"
|
||||
# ai-claude работает в дефолтном ~/.claude (нативный логин и аккаунты).
|
||||
# Модель хранится нативно в ~/.claude/settings.json; другие ai-* лаунчеры теперь
|
||||
# изолированы в своих CLAUDE_CONFIG_DIR, поэтому в пикер не протекают чужие модели -
|
||||
# показываются только нативные модели Claude Code.
|
||||
_apply_effort claude xhigh
|
||||
claude --dangerously-skip-permissions --system-prompt-file "$_PROMPT_FILE" "$@"
|
||||
CLAUDEEOF
|
||||
chmod +x "$BIN_DIR/ai-claude"
|
||||
|
||||
Reference in New Issue
Block a user