#!/usr/bin/env bash # ============================================================ # Claude Code Setup — GPT-5.5 / DeepSeek / Kimi / Gemini # Запуск: bash claude_setup.sh # ============================================================ CONFIG_DIR="$HOME/.config/claude-launcher" BIN_DIR="$HOME/.local/bin" NPM_GLOBAL="$HOME/.npm-global" PROXY_BIN="$BIN_DIR/claude-code-proxy" RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; NC='\033[0m' info() { echo -e "${CYAN}[INFO]${NC} $*"; } success() { echo -e "${GREEN}[OK]${NC} $*"; } warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } err() { echo -e "${RED}[ERR]${NC} $*"; exit 1; } # Запрет запуска от root if [ "$EUID" -eq 0 ]; then echo -e "${RED}Не запускайте этот скрипт через sudo!${NC}" echo "Запустите просто: bash claude_setup.sh" exit 1 fi info "Проверяю зависимости (python3)..." if ! command -v python3 &>/dev/null; then err "Python 3 не установлен, но он требуется для работы скрипта." fi success "Python 3 найден" # ── 1. npm prefix в домашнюю папку ────────────────────────── info "Настраиваю npm prefix..." mkdir -p "$NPM_GLOBAL" npm config set prefix "$NPM_GLOBAL" success "npm prefix -> $NPM_GLOBAL" export PATH="$NPM_GLOBAL/bin:$BIN_DIR:$PATH" # ── 2. Node.js ─────────────────────────────────────────────── info "Проверяю Node.js..." if ! command -v node &>/dev/null; then info "Попытка установки Node.js (нужен sudo)..." if command -v apt-get &>/dev/null; then curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - sudo apt-get install -y nodejs elif command -v dnf &>/dev/null; then sudo dnf install -y nodejs else warn "Не удалось определить пакетный менеджер. Установите Node.js вручную." fi fi if command -v node &>/dev/null; then success "Node.js $(node --version)" else warn "Node.js не найден. Некоторые функции могут не работать." fi # ── 3. Claude Code ─────────────────────────────────────────── info "Проверяю Claude Code..." if ! command -v claude &>/dev/null; then info "Устанавливаю Claude Code..." if curl -fsSL https://claude.ai/install.sh | bash 2>/dev/null; then success "Claude Code установлен (официальный инсталлер)" else npm install -g @anthropic-ai/claude-code success "Claude Code установлен (npm)" fi else success "Claude Code уже установлен: $(claude --version 2>/dev/null | head -1)" fi # ── 4. claude-code-proxy (GPT) ─────────────────────────────── mkdir -p "$BIN_DIR" install_proxy() { info "Устанавливаю claude-code-proxy..." ARCH=$(uname -m) case "$ARCH" in x86_64) ARCH_TAG="amd64" ;; aarch64) ARCH_TAG="arm64" ;; *) err "Неизвестная архитектура: $ARCH" ;; esac LATEST=$(curl -fsSL "https://api.github.com/repos/raine/claude-code-proxy/releases/latest" \ | grep '"tag_name"' | sed 's/.*"tag_name": *"\(.*\)".*/\1/') [ -z "$LATEST" ] && err "Не удалось получить версию claude-code-proxy с GitHub" TMP=$(mktemp -d) trap 'rm -rf "$TMP"' EXIT URL="https://github.com/raine/claude-code-proxy/releases/download/${LATEST}/claude-code-proxy-linux-${ARCH_TAG}.tar.gz" info "Скачиваю $URL" curl -fsSL "$URL" -o "$TMP/proxy.tar.gz" || err "Не удалось скачать claude-code-proxy" tar -xzf "$TMP/proxy.tar.gz" -C "$TMP" BINARY=$(find "$TMP" -name "claude-code-proxy" -type f | head -1) [ -z "$BINARY" ] && err "Бинарник не найден в архиве" cp "$BINARY" "$PROXY_BIN" chmod +x "$PROXY_BIN" success "claude-code-proxy $LATEST -> $PROXY_BIN" } if [ -f "$PROXY_BIN" ]; then CURRENT_VER=$("$PROXY_BIN" --version 2>/dev/null | head -1 || echo "unknown") success "claude-code-proxy уже установлен ($CURRENT_VER)" else install_proxy fi # ── 4b. effort-proxy wrapper (маппинг effort: xhigh→high) ───────── EFFORT_PROXY_BIN="$BIN_DIR/claude-gpt-effort-proxy.py" cat > "$EFFORT_PROXY_BIN" << 'PYEOF' #!/usr/bin/env python3 """Effort mapping proxy for GPT backend. claude-code-proxy now accepts: low, medium, high, max (no "xhigh"). Claude Code may send "xhigh" effort — we map it to "high". """ import http.client, http.server, sys, logging, socketserver UPSTREAM_PORT = int(sys.argv[1]) if len(sys.argv) > 1 else 18766 LISTEN_PORT = int(sys.argv[2]) if len(sys.argv) > 2 else 18765 logging.basicConfig( filename="/tmp/claude-gpt-effort-proxy.log", level=logging.INFO, format='{"t":"%(asctime)s","level":"%(levelname)s","msg":"%(message)s"}', datefmt='%Y-%m-%dT%H:%M:%S', ) log = logging.getLogger("effort-proxy") # Потокобезопасный сервер — обрабатывает несколько запросов одновременно class _ThreadedServer(socketserver.ThreadingMixIn, http.server.HTTPServer): daemon_threads = True class _Proxy(http.server.BaseHTTPRequestHandler): def proxy_request(self): body = b"" if cl := self.headers.get("Content-Length"): body = self.rfile.read(int(cl)) # Маппинг xhigh→high: claude-code-proxy больше не принимает xhigh body = body.replace(b'"xhigh"', b'"high"') try: conn = http.client.HTTPConnection("127.0.0.1", UPSTREAM_PORT, timeout=300) hdrs = dict(self.headers) hdrs.pop("Host", None) hdrs["Content-Length"] = str(len(body)) conn.request(self.command, self.path, body=body or None, headers=hdrs) resp = conn.getresponse() self.send_response(resp.status, resp.reason) chunked = False for k, v in resp.getheaders(): if k.lower() == "transfer-encoding" and "chunked" in v.lower(): chunked = True self.send_header(k, v) self.end_headers() while chunk := resp.read(4096): try: if chunked: self.wfile.write(f"{len(chunk):X}\r\n".encode() + chunk + b"\r\n") else: self.wfile.write(chunk) self.wfile.flush() except (BrokenPipeError, ConnectionResetError): break if chunked: try: self.wfile.write(b"0\r\n\r\n") except: pass conn.close() except Exception as e: log.error("proxy error: %s", e) try: self.send_error(502, str(e)) except: pass do_GET = do_POST = do_PUT = do_DELETE = do_HEAD = proxy_request def log_message(self, *args): pass log.info("effort-proxy starting on 127.0.0.1:%d → upstream %d", LISTEN_PORT, UPSTREAM_PORT) _ThreadedServer(("127.0.0.1", LISTEN_PORT), _Proxy).serve_forever() PYEOF chmod +x "$EFFORT_PROXY_BIN" success "claude-gpt-effort-proxy -> $EFFORT_PROXY_BIN" # ── 4c. openai-anthropic-proxy (Kimi) ────────────────────── OPENAI_ANTHROPIC_PROXY_BIN="$BIN_DIR/claude-openai-anthropic-proxy.py" cat > "$OPENAI_ANTHROPIC_PROXY_BIN" << 'PYEOF' #!/usr/bin/env python3 """Small Anthropic Messages → OpenAI Chat Completions proxy.""" import argparse import http.client import http.server import json import os import socketserver import sys import urllib.parse def _json_dumps(obj): return json.dumps(obj, ensure_ascii=False, separators=(",", ":")) def _read_json(handler): length = int(handler.headers.get("Content-Length", "0")) if length <= 0: return {} return json.loads(handler.rfile.read(length).decode("utf-8")) def _system_to_text(system): if isinstance(system, str): return system if isinstance(system, list): parts = [] for item in system: if isinstance(item, dict) and item.get("type") == "text": parts.append(item.get("text", "")) elif isinstance(item, str): parts.append(item) return "\n".join(p for p in parts if p) return "" def _content_to_openai(content): if isinstance(content, str): return content if not isinstance(content, list): return str(content) text_parts = [] for block in content: if not isinstance(block, dict): text_parts.append(str(block)) continue btype = block.get("type") if btype == "text": text_parts.append(block.get("text", "")) elif btype == "tool_result": value = block.get("content", "") if isinstance(value, list): value = "\n".join( part.get("text", "") if isinstance(part, dict) else str(part) for part in value ) text_parts.append(str(value)) return "\n".join(p for p in text_parts if p) def _messages_to_openai(body): messages = [] system = _system_to_text(body.get("system")) if system: messages.append({"role": "system", "content": system}) pending_tool_results = [] for msg in body.get("messages", []): role = msg.get("role", "user") content = msg.get("content", "") if isinstance(content, list) and role == "user": normal_blocks = [] for block in content: if isinstance(block, dict) and block.get("type") == "tool_result": tool_content = block.get("content", "") if isinstance(tool_content, list): tool_content = _content_to_openai(tool_content) pending_tool_results.append({ "role": "tool", "tool_call_id": block.get("tool_use_id", "tool_call"), "content": str(tool_content), }) else: normal_blocks.append(block) if normal_blocks: messages.append({"role": "user", "content": _content_to_openai(normal_blocks)}) messages.extend(pending_tool_results) pending_tool_results = [] continue converted = {"role": role, "content": _content_to_openai(content)} if isinstance(content, list) and role == "assistant": tool_calls = [] for block in content: if isinstance(block, dict) and block.get("type") == "tool_use": tool_calls.append({ "id": block.get("id", "tool_call"), "type": "function", "function": { "name": block.get("name", "tool"), "arguments": _json_dumps(block.get("input", {})), }, }) if tool_calls: converted["tool_calls"] = tool_calls messages.append(converted) return messages def _tools_to_openai(tools): result = [] for tool in tools or []: result.append({ "type": "function", "function": { "name": tool.get("name", "tool"), "description": tool.get("description", ""), "parameters": tool.get("input_schema", {"type": "object", "properties": {}}), }, }) return result def _tool_choice_to_openai(choice): if not choice: return None if isinstance(choice, str): return choice ctype = choice.get("type") if ctype == "auto": return "auto" if ctype == "any": return "required" if ctype == "tool": return {"type": "function", "function": {"name": choice.get("name", "")}} return None def _to_openai_request(body, model): out = { "model": model or body.get("model"), "messages": _messages_to_openai(body), "stream": bool(body.get("stream")), } if "max_tokens" in body: out["max_tokens"] = body["max_tokens"] for key in ("temperature", "top_p"): if key in body: out[key] = body[key] if body.get("stop_sequences"): out["stop"] = body["stop_sequences"] tools = _tools_to_openai(body.get("tools")) if tools: out["tools"] = tools choice = _tool_choice_to_openai(body.get("tool_choice")) if choice: out["tool_choice"] = choice return out def _finish_reason(reason): return { "stop": "end_turn", "length": "max_tokens", "tool_calls": "tool_use", "content_filter": "stop_sequence", }.get(reason or "stop", "end_turn") def _anthropic_response(openai_body, model): choice = (openai_body.get("choices") or [{}])[0] message = choice.get("message") or {} content = [] if message.get("content"): content.append({"type": "text", "text": message.get("content")}) for call in message.get("tool_calls") or []: fn = call.get("function") or {} raw_args = fn.get("arguments") or "{}" try: args = json.loads(raw_args) except Exception: args = {"arguments": raw_args} content.append({ "type": "tool_use", "id": call.get("id", "tool_call"), "name": fn.get("name", "tool"), "input": args, }) usage = openai_body.get("usage") or {} return { "id": openai_body.get("id", "msg_openai_proxy"), "type": "message", "role": "assistant", "model": model, "content": content or [{"type": "text", "text": ""}], "stop_reason": _finish_reason(choice.get("finish_reason")), "stop_sequence": None, "usage": { "input_tokens": usage.get("prompt_tokens", 0), "output_tokens": usage.get("completion_tokens", 0), }, } def _write_json(handler, status, obj): data = _json_dumps(obj).encode("utf-8") handler.send_response(status) handler.send_header("Content-Type", "application/json") handler.send_header("Content-Length", str(len(data))) handler.end_headers() handler.wfile.write(data) def _openai_error_to_anthropic(status, raw): try: data = json.loads(raw.decode("utf-8")) except Exception: data = {"error": raw.decode("utf-8", "replace")} err = data.get("error", data) if isinstance(err, dict): message = err.get("message") or _json_dumps(err) etype = err.get("type") or "api_error" else: message = str(err) etype = "api_error" return {"type": "error", "error": {"type": etype, "message": message}} class _ThreadedServer(socketserver.ThreadingMixIn, http.server.HTTPServer): daemon_threads = True class Proxy(http.server.BaseHTTPRequestHandler): upstream_base = "" api_key = "" model = "" def log_message(self, *args): pass def do_GET(self): path = urllib.parse.urlparse(self.path).path if path in ("/", "/health"): _write_json(self, 200, {"ok": True}) else: _write_json(self, 404, {"type": "error", "error": {"message": "not found"}}) def do_POST(self): path = urllib.parse.urlparse(self.path).path if not path.endswith("/messages"): _write_json(self, 404, {"type": "error", "error": {"message": "not found"}}) return try: body = _read_json(self) openai_body = _to_openai_request(body, self.model) if openai_body.get("stream"): self._proxy_stream(openai_body) else: self._proxy_json(openai_body) except Exception as exc: _write_json(self, 500, {"type": "error", "error": {"type": "proxy_error", "message": str(exc)}}) def _request_upstream(self, payload): parsed = urllib.parse.urlparse(self.upstream_base.rstrip("/") + "/chat/completions") conn_cls = http.client.HTTPSConnection if parsed.scheme == "https" else http.client.HTTPConnection conn = conn_cls(parsed.netloc, timeout=600) path = parsed.path or "/chat/completions" if parsed.query: path += "?" + parsed.query conn.request( "POST", path, body=_json_dumps(payload).encode("utf-8"), headers={ "Authorization": "Bearer " + self.api_key, "Content-Type": "application/json", }, ) return conn, conn.getresponse() def _proxy_json(self, payload): conn, resp = self._request_upstream(payload) raw = resp.read() conn.close() if resp.status >= 400: _write_json(self, resp.status, _openai_error_to_anthropic(resp.status, raw)) return _write_json(self, resp.status, _anthropic_response(json.loads(raw.decode("utf-8")), self.model)) def _sse(self, event, data): self.wfile.write(("event: " + event + "\n").encode("utf-8")) self.wfile.write(("data: " + _json_dumps(data) + "\n\n").encode("utf-8")) self.wfile.flush() def _proxy_stream(self, payload): conn, resp = self._request_upstream(payload) if resp.status >= 400: raw = resp.read() conn.close() _write_json(self, resp.status, _openai_error_to_anthropic(resp.status, raw)) return self.send_response(200) self.send_header("Content-Type", "text/event-stream") self.send_header("Cache-Control", "no-cache") self.end_headers() msg_id = "msg_openai_proxy" input_tokens = 0 output_tokens = 0 finish = "end_turn" text_started = False text_index = 0 tool_indexes = {} tool_buffers = {} next_index = 0 self._sse("message_start", { "type": "message_start", "message": { "id": msg_id, "type": "message", "role": "assistant", "model": self.model, "content": [], "stop_reason": None, "stop_sequence": None, "usage": {"input_tokens": 0, "output_tokens": 0}, }, }) self._sse("ping", {"type": "ping"}) while True: line = resp.readline() if not line: break line = line.decode("utf-8", "replace").strip() if not line or not line.startswith("data:"): continue data = line[5:].strip() if data == "[DONE]": break try: chunk = json.loads(data) except Exception: continue usage = chunk.get("usage") or {} input_tokens = usage.get("prompt_tokens", input_tokens) output_tokens = usage.get("completion_tokens", output_tokens) choice = (chunk.get("choices") or [{}])[0] if choice.get("finish_reason"): finish = _finish_reason(choice.get("finish_reason")) delta = choice.get("delta") or {} text = delta.get("content") if text: if not text_started: text_started = True text_index = next_index next_index += 1 self._sse("content_block_start", { "type": "content_block_start", "index": text_index, "content_block": {"type": "text", "text": ""}, }) self._sse("content_block_delta", { "type": "content_block_delta", "index": text_index, "delta": {"type": "text_delta", "text": text}, }) for call in delta.get("tool_calls") or []: cidx = call.get("index", 0) if cidx not in tool_indexes: tool_indexes[cidx] = next_index next_index += 1 tool_buffers[cidx] = {"id": call.get("id", "tool_call"), "name": "tool", "args": ""} buf = tool_buffers[cidx] if call.get("id"): buf["id"] = call["id"] fn = call.get("function") or {} if fn.get("name"): buf["name"] = fn["name"] if fn.get("arguments"): buf["args"] += fn["arguments"] conn.close() # Fallback: если модель потратила все токены на reasoning и не выдала # ни одного content-блока, отправляем пустой текстовый блок (Anthropic # требует минимум один content block в ответе). if not text_started and not tool_indexes: text_started = True text_index = next_index next_index += 1 self._sse("content_block_start", { "type": "content_block_start", "index": text_index, "content_block": {"type": "text", "text": ""}, }) self._sse("content_block_delta", { "type": "content_block_delta", "index": text_index, "delta": {"type": "text_delta", "text": ""}, }) if text_started: self._sse("content_block_stop", {"type": "content_block_stop", "index": text_index}) for cidx, index in sorted(tool_indexes.items(), key=lambda item: item[1]): buf = tool_buffers[cidx] try: parsed_args = json.loads(buf["args"] or "{}") except Exception: parsed_args = {"arguments": buf["args"]} self._sse("content_block_start", { "type": "content_block_start", "index": index, "content_block": { "type": "tool_use", "id": buf["id"], "name": buf["name"], "input": parsed_args, }, }) self._sse("content_block_stop", {"type": "content_block_stop", "index": index}) self._sse("message_delta", { "type": "message_delta", "delta": {"stop_reason": finish, "stop_sequence": None}, "usage": {"output_tokens": output_tokens}, }) self._sse("message_stop", {"type": "message_stop"}) def main(): parser = argparse.ArgumentParser() parser.add_argument("--host", default="127.0.0.1") parser.add_argument("--port", type=int, default=int(os.environ.get("OPENAI_ANTHROPIC_PROXY_PORT", "18767"))) args = parser.parse_args() Proxy.upstream_base = os.environ.get("OPENAI_BASE_URL", "https://api.artemox.com/v1") Proxy.api_key = os.environ.get("OPENAI_API_KEY", "") Proxy.model = os.environ.get("OPENAI_MODEL", "kimi-k2.6") if not Proxy.api_key: print("OPENAI_API_KEY is required", file=sys.stderr) return 1 _ThreadedServer((args.host, args.port), Proxy).serve_forever() return 0 if __name__ == "__main__": raise SystemExit(main()) PYEOF chmod +x "$OPENAI_ANTHROPIC_PROXY_BIN" success "claude-openai-anthropic-proxy -> $OPENAI_ANTHROPIC_PROXY_BIN" # ── 5. antigravity-claude-proxy (Gemini) ──────────────────── info "Проверяю antigravity-claude-proxy..." if command -v antigravity-claude-proxy &>/dev/null || command -v acc &>/dev/null; then success "antigravity-claude-proxy уже установлен" else info "Устанавливаю antigravity-claude-proxy (npm, без sudo)..." npm install -g antigravity-claude-proxy@latest success "antigravity-claude-proxy установлен" fi # ── 6. Папка для конфигов ──────────────────────────────────── mkdir -p "$CONFIG_DIR" # ── 7. Очистка старых функций из .bashrc / .zshrc ─────────── clean_rc() { local rc_file="$1" if [ -f "$rc_file" ] && grep -q "# === CLAUDE LAUNCHER ===" "$rc_file"; then info "Очищаю старые функции из $rc_file..." python3 - "$rc_file" <<'PYEOF' import sys path = sys.argv[1] with open(path, 'r') as f: content = f.read() marker = '# === CLAUDE LAUNCHER ===' end_marker = '# === END CLAUDE LAUNCHER ===' start = content.find(marker) end = content.find(end_marker) if start != -1 and end != -1: new_content = content[:start] + content[end + len(end_marker):].lstrip('\n') with open(path, 'w') as f: f.write(new_content) PYEOF success "Старые функции удалены из $rc_file" fi } clean_rc "$HOME/.bashrc" clean_rc "$HOME/.zshrc" add_path_to_rc() { local rc_file="$1" if [ -f "$rc_file" ]; then if ! grep -q 'NPM_GLOBAL' "$rc_file" 2>/dev/null; then cat >> "$rc_file" << 'PATHEOF' # Claude Code Launcher PATH export NPM_GLOBAL="$HOME/.npm-global" export PATH="$NPM_GLOBAL/bin:$HOME/.local/bin:$PATH" PATHEOF success "PATH добавлен в $rc_file" fi fi } add_path_to_rc "$HOME/.bashrc" [ -f "$HOME/.zshrc" ] && add_path_to_rc "$HOME/.zshrc" # ── 8. Генерация Standalone скриптов ──────────────────────── info "Генерирую standalone скрипты в $BIN_DIR..." HELPERS_FILE="$BIN_DIR/claude_api_helpers.sh" cat > "$HELPERS_FILE" << 'HELPEREOF' #!/usr/bin/env bash # _claude_test_api: Send 1-token test to an Anthropic-compatible endpoint _claude_test_api() { local url="$1" auth_header="$2" model="${3:-claude-sonnet-4-6}" local response response=$(curl -s -w "\n%{http_code}" --max-time 15 "$url" \ -H "$auth_header" \ -H "Content-Type: application/json" \ -H "anthropic-version: 2023-06-01" \ -d "{\"model\":\"$model\",\"max_tokens\":1,\"messages\":[{\"role\":\"user\",\"content\":\"hi\"}]}" \ 2>/dev/null || echo "000") _CLAUDE_TEST_CODE=$(echo "$response" | tail -1) _CLAUDE_TEST_BODY=$(echo "$response" | sed '$d') } # _claude_test_openai_api: Send a tiny test to an OpenAI-compatible endpoint _claude_test_openai_api() { local url="$1" api_key="$2" model="${3:-kimi-k2.6}" local response response=$(curl -s -w "\n%{http_code}" --max-time 30 "$url" \ -H "Authorization: Bearer $api_key" \ -H "Content-Type: application/json" \ -d "{\"model\":\"$model\",\"max_tokens\":1,\"messages\":[{\"role\":\"user\",\"content\":\"hi\"}]}" \ 2>/dev/null || echo "000") _CLAUDE_TEST_CODE=$(echo "$response" | tail -1) _CLAUDE_TEST_BODY=$(echo "$response" | sed '$d') } _handle_openai_api_response() { local provider="$1" local code="$2" local body="$3" local topup_url="$4" local _emsg case "$code" in 200) echo -e "\033[0;32mOK\033[0m" return 0 ;; 401|403) _emsg=$(_claude_extract_error "$body") echo "" echo -e "\033[0;31m[ОШИБКА АВТОРИЗАЦИИ]\033[0m Авторизация $provider недействительна (HTTP $code)." [ -n "$_emsg" ] && echo " $_emsg" return 401 ;; 429) _emsg=$(_claude_extract_error "$body") echo "" echo -e "\033[0;33m[ЛИМИТ ИСЧЕРПАН]\033[0m Баланс/лимит $provider исчерпан." [ -n "$_emsg" ] && echo " $_emsg" [ -n "$topup_url" ] && echo " $topup_url" return 429 ;; 000) echo "" echo -e "\033[0;33m[СЕТЬ]\033[0m Не удалось проверить ключ (нет сети?). Продолжаю..." return 0 ;; *) _emsg=$(_claude_extract_error "$body") echo "" echo -e "\033[0;31m[ОШИБКА]\033[0m API $provider вернул HTTP $code." [ -n "$_emsg" ] && echo " $_emsg" return 1 ;; esac } _claude_extract_error() { local body="$1" echo "$body" | python3 -c " import sys, json try: d = json.load(sys.stdin) e = d.get('error', {}) if isinstance(e, str): print(e) else: msg = e.get('message', '') if msg: print(msg) except: pass " 2>/dev/null } _claude_offer_reauth() { local provider="$1" echo "" echo -n "Хотите выполнить повторную авторизацию $provider? [Y/n] " local answer read -r answer case "${answer:-Y}" in [Yy]|[Yy][Ee][Ss]) return 0 ;; *) echo "Отменено."; return 1 ;; esac } _handle_api_response() { local provider="$1" local code="$2" local body="$3" local topup_url="$4" local _emsg case "$code" in 200) echo -e "\033[0;32mOK\033[0m" return 0 ;; 401|403) _emsg=$(_claude_extract_error "$body") echo "" echo -e "\033[0;31m[ОШИБКА АВТОРИЗАЦИИ]\033[0m Авторизация $provider недействительна (HTTP $code)." [ -n "$_emsg" ] && echo " $_emsg" return 401 ;; 429) _emsg=$(_claude_extract_error "$body") echo "" echo -e "\033[0;33m[ЛИМИТ ИСЧЕРПАН]\033[0m Баланс/лимит $provider исчерпан." [ -n "$_emsg" ] && echo " $_emsg" [ -n "$topup_url" ] && echo " $topup_url" return 429 ;; 000) echo "" echo -e "\033[0;33m[СЕТЬ]\033[0m Не удалось проверить ключ (нет сети?). Продолжаю..." return 0 ;; 400) _emsg=$(_claude_extract_error "$body") if echo "${_emsg:-$body}" | grep -qi "RESOURCE_EXHAUSTED"; then echo "" echo -e "\033[0;33m[КВОТА ИСЧЕРПАНА]\033[0m Лимит запросов исчерпан." [ -n "$topup_url" ] && echo " $topup_url" return 429 fi # 400 = auth is valid, but max_tokens=1 is too small for thinking models echo -e "\033[0;32mOK\033[0m" return 0 ;; *) _emsg=$(_claude_extract_error "$body") echo "" echo -e "\033[0;31m[ОШИБКА]\033[0m API $provider вернул HTTP $code." [ -n "$_emsg" ] && echo " $_emsg" return 1 ;; esac } _open_browser() { local url="$1" if command -v xdg-open &>/dev/null; then xdg-open "$url" 2>/dev/null elif command -v open &>/dev/null; then open "$url" 2>/dev/null elif command -v sensible-browser &>/dev/null; then sensible-browser "$url" 2>/dev/null else echo "Откройте вручную: $url"; fi } HELPEREOF chmod +x "$HELPERS_FILE" # === claude_gpt === cat > "$BIN_DIR/claude_gpt" << 'GPTEOF' #!/usr/bin/env bash # claude_gpt — запуск нативного OpenAI Codex codex_bin="$HOME/.npm-global/bin/codex" [ ! -f "$codex_bin" ] && codex_bin="$(command -v codex 2>/dev/null)" if [ -z "$codex_bin" ] || [ ! -f "$codex_bin" ]; then echo "Ошибка: OpenAI Codex не найден." echo "Установите: npm install -g @openai/codex" exit 1 fi exec "$codex_bin" "$@" GPTEOF chmod +x "$BIN_DIR/claude_gpt" # === claude_deepseek === cat > "$BIN_DIR/claude_deepseek" << 'DEEPSEEKEOF' #!/usr/bin/env bash source ~/.local/bin/claude_api_helpers.sh key_file="$HOME/.config/claude-launcher/deepseek_key" api_key="" reauth=0 [ -f "$key_file" ] && api_key=$(cat "$key_file") if [ -n "$api_key" ]; then echo -n "Проверка сохранённого DeepSeek ключа... " _claude_test_api "https://api.deepseek.com/anthropic/v1/messages" "x-api-key: $api_key" "deepseek-v4-flash" _handle_api_response "DeepSeek" "$_CLAUDE_TEST_CODE" "$_CLAUDE_TEST_BODY" "Пополните баланс: https://platform.deepseek.com/top_up" 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 "Хотите ввести новый DeepSeek ключ? [Y/n] " read -r _ans; case "${_ans:-Y}" in [Yy]*) ;; *) exit 1 ;; esac fi if [ -z "$api_key" ]; then echo "Получить ключ: https://platform.deepseek.com/api_keys" read -r -p "Введите ваш DeepSeek API ключ: " api_key [ -z "$api_key" ] && { echo "Выход."; exit 1; } echo -n "Проверяю ключ и баланс... " _claude_test_api "https://api.deepseek.com/anthropic/v1/messages" "x-api-key: $api_key" "deepseek-v4-flash" _handle_api_response "DeepSeek" "$_CLAUDE_TEST_CODE" "$_CLAUDE_TEST_BODY" "Пополните баланс: https://platform.deepseek.com/top_up" 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 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_SONNET_MODEL=deepseek-v4-pro \ ANTHROPIC_DEFAULT_HAIKU_MODEL=deepseek-v4-flash \ CLAUDE_CODE_SUBAGENT_MODEL=deepseek-v4-flash \ CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 \ claude "$@" DEEPSEEKEOF chmod +x "$BIN_DIR/claude_deepseek" # === claude_kimi === cat > "$BIN_DIR/claude_kimi" << 'KIMIEOF' #!/usr/bin/env bash source ~/.local/bin/claude_api_helpers.sh key_file="$HOME/.config/claude-launcher/kimi_key" api_key="" reauth=0 [ -f "$key_file" ] && api_key=$(cat "$key_file") if [ -n "$api_key" ]; then echo -n "Проверка сохранённого Kimi ключа... " _claude_test_openai_api "https://api.artemox.com/v1/chat/completions" "$api_key" "kimi-k2.6" _handle_openai_api_response "Kimi" "$_CLAUDE_TEST_CODE" "$_CLAUDE_TEST_BODY" "Пополните баланс: https://artemox.com/dashboard" 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 "Хотите ввести новый Kimi ключ? [Y/n] " read -r _ans; case "${_ans:-Y}" in [Yy]*) ;; *) exit 1 ;; esac fi if [ -z "$api_key" ]; then echo "Получить ключ: https://artemox.com/dashboard" read -r -p "Введите ваш Artemox API ключ: " api_key [ -z "$api_key" ] && { echo "Выход."; exit 1; } echo -n "Проверяю ключ и баланс... " _claude_test_openai_api "https://api.artemox.com/v1/chat/completions" "$api_key" "kimi-k2.6" _handle_openai_api_response "Kimi" "$_CLAUDE_TEST_CODE" "$_CLAUDE_TEST_BODY" "Пополните баланс: https://artemox.com/dashboard" 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 # Запускаем прокси proxy_bin="$HOME/.local/bin/claude-openai-anthropic-proxy.py" proxy_port=18767 proxy_pid="" cleanup() { [ -n "$proxy_pid" ] && kill "$proxy_pid" 2>/dev/null; } trap cleanup EXIT INT TERM OPENAI_API_KEY="$api_key" \ OPENAI_MODEL=kimi-k2.6 \ python3 "$proxy_bin" --port "$proxy_port" &>/tmp/claude-openai-anthropic-proxy.log & proxy_pid=$! echo -n "Запуск Kimi прокси... " _i=0; while [ $_i -lt 10 ]; do sleep 1; curl -s --max-time 1 "http://localhost:$proxy_port/" &>/dev/null; [ "$?" -ne 7 ] && break; _i=$((_i + 1)); done if [ $_i -ge 10 ]; then echo -e "\033[0;31m[ОШИБКА]\033[0m Kimi прокси не запустился за 10 сек." echo "Лог прокси:" cat /tmp/claude-openai-anthropic-proxy.log 2>/dev/null exit 1 fi echo -e "\033[0;32mOK\033[0m" ANTHROPIC_BASE_URL="http://localhost:$proxy_port" \ ANTHROPIC_AUTH_TOKEN=dummy \ ANTHROPIC_MODEL=kimi-k2.6 \ ANTHROPIC_DEFAULT_OPUS_MODEL=kimi-k2.6 \ ANTHROPIC_DEFAULT_SONNET_MODEL=kimi-k2.6 \ ANTHROPIC_DEFAULT_HAIKU_MODEL=kimi-k2.6 \ CLAUDE_CODE_SUBAGENT_MODEL=kimi-k2.6 \ CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 \ claude "$@" KIMIEOF chmod +x "$BIN_DIR/claude_kimi" # === claude_gemini === cat > "$BIN_DIR/claude_gemini" << 'GEMINIEOF' #!/usr/bin/env bash source ~/.local/bin/claude_api_helpers.sh acc_cmd="" command -v antigravity-claude-proxy &>/dev/null && acc_cmd="antigravity-claude-proxy" command -v acc &>/dev/null && acc_cmd="acc" [ -z "$acc_cmd" ] && { echo "Ошибка: antigravity-claude-proxy не найден."; exit 1; } proxy_pid="" cleanup() { [ -n "$proxy_pid" ] && kill "$proxy_pid" 2>/dev/null; } trap cleanup EXIT INT TERM if ! curl -sf http://localhost:8080/health &>/dev/null; then "$acc_cmd" start &>/tmp/antigravity-proxy.log & proxy_pid=$! echo "Запускаю Gemini прокси..." i=0; while [ $i -lt 15 ]; do sleep 1; curl -sf http://localhost:8080/health &>/dev/null && break; i=$((i+1)); done fi has_auth=$(curl -sf "http://localhost:8080/account-limits" 2>/dev/null || echo "") if [ -n "$has_auth" ]; then total_count=$(echo "$has_auth" | python3 -c "import sys, json; print(len(json.load(sys.stdin).get('accounts', [])))" 2>/dev/null || echo "0") invalid_count=$(echo "$has_auth" | python3 -c "import sys, json; print(len([a for a in json.load(sys.stdin).get('accounts', []) if a.get('isInvalid')]))" 2>/dev/null || echo "0") fi if [ -z "$has_auth" ] || [ "$total_count" = "0" ]; then echo -e "\033[1;33m⚠️ ВНИМАНИЕ: Используйте ОТДЕЛЬНЫЙ Google-аккаунт!\033[0m" _open_browser "http://localhost:8080" echo "Нажмите Enter после завершения авторизации в браузере..." read -r elif [ "$invalid_count" -gt 0 ]; then echo -e "\033[0;33m[ПРЕДУПРЕЖДЕНИЕ]\033[0m Обнаружены проблемные аккаунты ($invalid_count из $total_count)." fi echo -n "Проверка доступа к Gemini API... " _claude_test_api "http://localhost:8080/v1/messages" "x-api-key: dummy" "gemini-pro-agent" if [ "$_CLAUDE_TEST_CODE" != "400" ]; then _handle_api_response "Gemini" "$_CLAUDE_TEST_CODE" "$_CLAUDE_TEST_BODY" "" ret=$? if [ $ret -ne 0 ]; then if _claude_offer_reauth "Gemini"; then _open_browser "http://localhost:8080" echo "Нажмите Enter после авторизации/добавления..." read -r _claude_test_api "http://localhost:8080/v1/messages" "x-api-key: dummy" "gemini-pro-agent" [ "$_CLAUDE_TEST_CODE" != "200" ] && [ "$_CLAUDE_TEST_CODE" != "400" ] && { echo "ОШИБКА"; exit 1; } echo -e "\033[0;32mOK\033[0m" else exit 1 fi fi else _emsg=$(_claude_extract_error "$_CLAUDE_TEST_BODY") if echo "$_emsg" | grep -q "RESOURCE_EXHAUSTED"; then echo -e "\033[0;33m[КВОТА ИСЧЕРПАНА]\033[0m Все Gemini аккаунты исчерпали лимит запросов." exit 1 fi echo -e "\033[0;32mOK\033[0m" fi ANTHROPIC_BASE_URL=http://localhost:8080 \ ANTHROPIC_AUTH_TOKEN=dummy \ ANTHROPIC_MODEL=gemini-pro-agent \ ANTHROPIC_DEFAULT_OPUS_MODEL=gemini-pro-agent \ ANTHROPIC_DEFAULT_SONNET_MODEL=gemini-pro-agent \ ANTHROPIC_DEFAULT_HAIKU_MODEL=gemini-3.5-flash-low \ CLAUDE_CODE_SUBAGENT_MODEL=gemini-3.5-flash-low \ CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 \ claude "$@" GEMINIEOF chmod +x "$BIN_DIR/claude_gemini" success "Скрипты сгенерированы." # ── 9. Итог ────────────────────────────────────────────────── echo "" echo -e "${GREEN}════════════════════════════════════════════════════${NC}" echo -e "${GREEN} Установка завершена!${NC}" echo -e "${GREEN}════════════════════════════════════════════════════${NC}" echo "" echo "Доступные команды (теперь это независимые скрипты в ~/.local/bin):" echo -e " ${CYAN}claude_gpt${NC} — OpenAI Codex (нативный CLI)" echo -e " ${CYAN}claude_deepseek${NC} — DeepSeek (API ключ сохраняется)" echo -e " ${CYAN}claude_kimi${NC} — Kimi K2.6 (Artemox API, ключ сохраняется)" echo -e " ${CYAN}claude_gemini${NC} — Gemini (Google OAuth через браузер)" echo "" echo -e "${YELLOW}⚠️ Для Gemini используйте отдельный Google-аккаунт!${NC}" echo ""