diff --git a/home-configs/claude/statusline-command.sh b/home-configs/claude/statusline-command.sh index eca42c4..3899947 100755 --- a/home-configs/claude/statusline-command.sh +++ b/home-configs/claude/statusline-command.sh @@ -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') diff --git a/scripts/ai-setup.sh b/scripts/ai-setup.sh index d93d833..02ab9ee 100755 --- a/scripts/ai-setup.sh +++ b/scripts/ai-setup.sh @@ -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