feat: поддержка баланса OpenRouter в statusline + обновление модели-пикера

- Добавлен _openrouter_balance() в ai-api-helpers.sh
- Statusline теперь показывает баланс для openrouter лаунчера
- Обновлены дефолтные модели в ai-openrouter (Grok 4.20 как opus и т.д.)
- Улучшена изоляция конфигов для deepseek/kimi/openrouter

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-06-12 11:19:29 +03:00
parent 648c9f068b
commit dfa706e7a0
2 changed files with 113 additions and 11 deletions

View File

@@ -170,6 +170,45 @@ for info in infos:
parts.append(f'{sym}{total}')
if parts:
print(' '.join(parts))
" 2>/dev/null)
if [ -n "$new_balance" ]; then
echo "$new_balance" > "$cache_file"
fi
fi
) &
fi
fi
elif [ "${AI_LAUNCHER:-}" = "openrouter" ]; then
# --- Баланс OpenRouter ---
# Моментально показываем кэшированный остаток, в фоне обновляем через API.
cache_file="$HOME/.cache/ai-setup/openrouter_balance"
if [ -f "$cache_file" ]; then
balance=$(head -1 "$cache_file")
[ -n "$balance" ] && printf " \033[38;5;78m%s\033[00m" "$balance"
fi
# Фоновое обновление баланса (не чаще раза в 30 секунд)
refresh_ts="$HOME/.cache/ai-setup/openrouter_balance_refresh_ts"
now=$(date +%s)
last=$(cat "$refresh_ts" 2>/dev/null || echo 0)
if [ $(( now - last )) -gt 30 ]; then
key_file="$HOME/.config/ai-setup/openrouter_key"
if [ -f "$key_file" ]; then
echo "$now" > "$refresh_ts" 2>/dev/null
(
api_key=$(cat "$key_file")
resp=$(curl -s --max-time 10 "https://openrouter.ai/api/v1/credits" \
-H "Authorization: Bearer $api_key" \
-H "Accept: application/json" 2>/dev/null)
if [ -n "$resp" ]; then
new_balance=$(echo "$resp" | python3 -c "
import sys, json
d = json.load(sys.stdin)
data = d.get('data', {})
total = data.get('total_credits', 0) or 0
usage = data.get('total_usage', 0) or 0
remaining = total - usage
print(f'${remaining:.2f}')
" 2>/dev/null)
if [ -n "$new_balance" ]; then
echo "$new_balance" > "$cache_file"
@@ -179,7 +218,7 @@ if parts:
fi
fi
else
# Рейт-лимиты для НЕ-DeepSeek провайдеров
# Рейт-лимиты для НЕ-DeepSeek/OpenRouter провайдеров
# Кеш специфичен для провайдера (model_id) И аккаунта (account): лимиты привязаны
# к аккаунту, поэтому при переключении /switch-account проценты не должны смешиваться.
_cache_key=$(echo "${model_id:-unknown}_${account:-}" | sed 's/[^a-zA-Z0-9._-]/_/g')

View File

@@ -966,6 +966,39 @@ except Exception as e:
" 2>/dev/null || echo -e " \033[0;33m[БАЛАНС]\033[0m Ошибка парсинга ответа"
}
# _openrouter_balance: Query OpenRouter credits API and print balance
# Uses /api/v1/credits (works with regular API key too)
_openrouter_balance() {
local api_key="$1"
local response
response=$(curl -s --max-time 10 "https://openrouter.ai/api/v1/credits" \
-H "Authorization: Bearer $api_key" \
-H "Accept: application/json" \
2>/dev/null || echo "")
if [ -z "$response" ]; then
echo -e " \033[0;33m[БАЛАНС]\033[0m Не удалось получить баланс (сеть?)"
return 1
fi
echo "$response" | python3 -c "
import sys, json, os
try:
d = json.load(sys.stdin)
data = d.get('data', {})
total = data.get('total_credits', 0) or 0
usage = data.get('total_usage', 0) or 0
remaining = total - usage
cache_dir = os.path.expanduser('~/.cache/ai-setup')
os.makedirs(cache_dir, exist_ok=True)
cache_file = os.path.join(cache_dir, 'openrouter_balance')
# Зелёный как у DeepSeek: \033[38;5;78m
print(f' \033[1;36m💰 Баланс OpenRouter:\033[0m \033[38;5;78m\${remaining:.2f}\033[0m (загружено \${total:.2f}, потрачено \${usage:.2f})')
with open(cache_file, 'w') as f:
f.write(f'\${remaining:.2f}\n')
except Exception as e:
print(f' ⚠️ Не удалось разобрать баланс: {e}')
" 2>/dev/null || echo -e " \033[0;33m[БАЛАНС]\033[0m Ошибка парсинга ответа"
}
_handle_openai_api_response() {
local provider="$1"
local code="$2"
@@ -1337,8 +1370,12 @@ trap 'rm -f "$_PROMPT_FILE"' EXIT INT TERM
_build_ai_sys_prompt > "$_PROMPT_FILE"
export AI_LAUNCHER=deepseek
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"]'
# Пикер: DeepSeek V4 Pro (opus+sonnet, дефолт) и DeepSeek V4 Flash (haiku).
# availableModels НЕ задаём: при кастомном провайдере он схлопывает пикер в Default.
# Claude Code навязывает 3 слота opus/sonnet/haiku; незаданный слот показал бы чужой
# Claude, поэтому sonnet тоже мапим на Pro (лёгкий дубль, но все пункты - DeepSeek).
# FABLE не навязывается - не задаём. DISABLE_1M убирает [1M] дубли из пикера.
_setup_isolated_config deepseek opus high ''
_apply_effort deepseek high
ANTHROPIC_BASE_URL=https://api.deepseek.com/anthropic \
ANTHROPIC_AUTH_TOKEN="$api_key" \
@@ -1346,10 +1383,13 @@ 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_SONNET_MODEL_NAME="DeepSeek V4 Pro" \
ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION="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_1M_CONTEXT=1 \
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 \
claude --dangerously-skip-permissions --system-prompt-file "$_PROMPT_FILE" "$@"
DEEPSEEKEOF
@@ -1417,8 +1457,11 @@ trap 'rm -f "$_PROMPT_FILE"' EXIT INT TERM
_build_ai_sys_prompt > "$_PROMPT_FILE"
export AI_LAUNCHER=kimi
export CLAUDE_CONFIG_DIR="$HOME/.config/ai-setup/cfg/kimi"
# Пикер: единственная модель Kimi K2.6 (под алиасом opus)
_setup_isolated_config kimi opus high '["opus"]'
# Пикер: Kimi K2.6 - единственная модель провайдера. availableModels НЕ задаём
# (он схлопывает пикер в Default). Claude Code навязывает 3 слота opus/sonnet/haiku;
# незаданный показал бы чужой Claude, поэтому все три мапим на Kimi K2.6
# (в пикере 3 одинаковых пункта, но все - Kimi). DISABLE_1M убирает [1M] дубли.
_setup_isolated_config kimi opus high ''
_apply_effort kimi high
ANTHROPIC_BASE_URL=https://api.kimi.com/coding \
ANTHROPIC_AUTH_TOKEN="$api_key" \
@@ -1426,8 +1469,13 @@ 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_SONNET_MODEL_NAME="Kimi K2.6" \
ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION="Kimi K2.6 (Moonshot AI)" \
ANTHROPIC_DEFAULT_HAIKU_MODEL=kimi-k2.6 \
ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME="Kimi K2.6" \
ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION="Kimi K2.6 (Moonshot AI)" \
CLAUDE_CODE_SUBAGENT_MODEL=kimi-k2.6 \
CLAUDE_CODE_DISABLE_1M_CONTEXT=1 \
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 \
claude --dangerously-skip-permissions --system-prompt-file "$_PROMPT_FILE" "$@"
KIMIEOF
@@ -1453,10 +1501,13 @@ if [ -n "$api_key" ]; then
rm -f "$key_file"
api_key=""
elif [ $ret -eq 429 ]; then
_openrouter_balance "$api_key"
echo -n "Продолжить всё равно? (запросы могут не проходить) [y/N] "
read -r _ans; case "${_ans:-N}" in [Yy]*) ;; *) exit 1 ;; esac
elif [ $ret -ne 0 ]; then
exit 1
else
_openrouter_balance "$api_key"
fi
fi
@@ -1474,6 +1525,7 @@ if [ -z "$api_key" ]; then
echo "$api_key" > "$key_file"
chmod 600 "$key_file"
echo "Ключ сохранён."
_openrouter_balance "$api_key"
if [ $ret -eq 429 ]; then
echo -n "Продолжить всё равно? (запросы могут не проходить) [y/N] "
read -r _ans; case "${_ans:-N}" in [Yy]*) ;; *) exit 1 ;; esac
@@ -1495,19 +1547,30 @@ trap 'rm -f "$_PROMPT_FILE"' EXIT INT TERM
_build_ai_sys_prompt > "$_PROMPT_FILE"
export AI_LAUNCHER=openrouter
export CLAUDE_CONFIG_DIR="$HOME/.config/ai-setup/cfg/openrouter"
# openrouter - гибкий лаунчер: пикер не ограничиваем (availableModels пустой),
# gpt-5.5 добавляем отдельным пунктом и делаем дефолтом
# openrouter - модели, которых НЕТ в других ai-* лаунчерах (без anthropic/deepseek/
# kimi/gemini). Пикер строится из 4 алиас-слотов + 1 custom-пункта (потолок Claude Code).
# availableModels НЕ задаём (он схлопывает пикер). Дефолт - GPT-5.5 (custom-пункт).
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"
export ANTHROPIC_CUSTOM_MODEL_OPTION_DESCRIPTION="OpenAI GPT-5.5 (OpenRouter)"
_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_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 \
ANTHROPIC_DEFAULT_OPUS_MODEL=x-ai/grok-4.20 \
ANTHROPIC_DEFAULT_OPUS_MODEL_NAME="Grok 4.20" \
ANTHROPIC_DEFAULT_OPUS_MODEL_DESCRIPTION="xAI Grok 4.20 (OpenRouter)" \
ANTHROPIC_DEFAULT_SONNET_MODEL=qwen/qwen3.7-max \
ANTHROPIC_DEFAULT_SONNET_MODEL_NAME="Qwen3.7 Max" \
ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION="Qwen3.7 Max (OpenRouter)" \
ANTHROPIC_DEFAULT_HAIKU_MODEL=minimax/minimax-m3 \
ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME="MiniMax M3" \
ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION="MiniMax M3 (OpenRouter)" \
ANTHROPIC_DEFAULT_FABLE_MODEL=meta-llama/llama-4-maverick \
ANTHROPIC_DEFAULT_FABLE_MODEL_NAME="Llama 4 Maverick" \
ANTHROPIC_DEFAULT_FABLE_MODEL_DESCRIPTION="Meta Llama 4 Maverick (OpenRouter)" \
CLAUDE_CODE_SUBAGENT_MODEL=openai/gpt-5.5 \
CLAUDE_CODE_DISABLE_1M_CONTEXT=1 \
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 \
claude --dangerously-skip-permissions --system-prompt-file "$_PROMPT_FILE" "$@"
OPENROUTEREOF