- Add claude-openai-anthropic-proxy.py (Anthropic ↔ OpenAI translator) - Rewrite claude_kimi to use native kimi CLI instead of Claude wrapper - Add OpenAI-compatible API test helpers (_claude_test_openai_api, _handle_openai_api_response) - Replace 127.0.0.1 with localhost for broader compatibility - Add test for query-string handling in Kimi proxy - Update README to reflect Artemox API usage
1152 lines
42 KiB
Bash
Executable File
1152 lines
42 KiB
Bash
Executable File
#!/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
|
||
source ~/.local/bin/claude_api_helpers.sh
|
||
|
||
proxy_bin="$HOME/.local/bin/claude-code-proxy"
|
||
if [ ! -f "$proxy_bin" ]; then
|
||
echo "Ошибка: claude-code-proxy не найден. Перезапустите claude_setup.sh"
|
||
exit 1
|
||
fi
|
||
|
||
if ! "$proxy_bin" codex auth status &>/dev/null; then
|
||
echo ""
|
||
echo "Авторизация ChatGPT не найдена."
|
||
echo "Сейчас появится ссылка или откроется браузер..."
|
||
echo ""
|
||
if ! "$proxy_bin" codex auth login 2>&1; then
|
||
echo "Если браузер не открылся, попробуйте device flow: claude-code-proxy codex auth device"
|
||
exit 1
|
||
fi
|
||
fi
|
||
|
||
proxy_pid=""
|
||
wrapper_pid=""
|
||
effort_proxy="$HOME/.local/bin/claude-gpt-effort-proxy.py"
|
||
|
||
cleanup() {
|
||
[ -n "$proxy_pid" ] && kill "$proxy_pid" 2>/dev/null
|
||
[ -n "$wrapper_pid" ] && kill "$wrapper_pid" 2>/dev/null
|
||
}
|
||
trap cleanup EXIT INT TERM
|
||
|
||
# Всегда принудительно убираем старый effort-proxy (может висеть после краша)
|
||
pkill -f "claude-gpt-effort-proxy" 2>/dev/null
|
||
sleep 0.3
|
||
|
||
# Запускаем claude-code-proxy если не запущен
|
||
if ! pgrep -f "claude-code-proxy serve" &>/dev/null; then
|
||
PORT=18766 "$proxy_bin" serve &>/tmp/claude-code-proxy.log &
|
||
proxy_pid=$!
|
||
# curl exit 7 = connection refused; any other response means the port is alive
|
||
_j=0; while [ $_j -lt 10 ]; do sleep 1; curl -s --max-time 1 http://localhost:18766/ &>/dev/null; [ "$?" -ne 7 ] && break; _j=$((_j + 1)); done
|
||
if [ $_j -ge 10 ]; then
|
||
echo -e "\033[0;31m[ОШИБКА]\033[0m claude-code-proxy не запустился за 10 сек."
|
||
exit 1
|
||
fi
|
||
fi
|
||
|
||
# Запускаем effort-proxy (всегда свежий экземпляр)
|
||
python3 "$effort_proxy" 18766 18765 &>/tmp/claude-gpt-effort-proxy.log &
|
||
wrapper_pid=$!
|
||
_i=0; while [ $_i -lt 10 ]; do sleep 1; curl -s --max-time 1 http://localhost:18765/ &>/dev/null; [ "$?" -ne 7 ] && break; _i=$((_i + 1)); done
|
||
if [ $_i -ge 10 ]; then
|
||
echo -e "\033[0;31m[ОШИБКА]\033[0m claude-gpt-effort-proxy не запустился за 10 сек."
|
||
echo "Лог effort-proxy:"
|
||
cat /tmp/claude-gpt-effort-proxy.log 2>/dev/null
|
||
exit 1
|
||
fi
|
||
|
||
echo -n "Проверка авторизации ChatGPT... "
|
||
_claude_test_api "http://localhost:18765/v1/messages" "x-api-key: dummy" "gpt-5.4-mini"
|
||
|
||
if [ "$_CLAUDE_TEST_CODE" != "400" ]; then
|
||
_handle_api_response "ChatGPT" "$_CLAUDE_TEST_CODE" "$_CLAUDE_TEST_BODY" ""
|
||
ret=$?
|
||
if [ $ret -eq 401 ]; then
|
||
"$proxy_bin" codex auth logout 2>/dev/null
|
||
if _claude_offer_reauth "ChatGPT"; then
|
||
"$proxy_bin" codex auth login || exit 1
|
||
_claude_test_api "http://localhost:18765/v1/messages" "x-api-key: dummy" "gpt-5.4-mini"
|
||
[ "$_CLAUDE_TEST_CODE" != "200" ] && [ "$_CLAUDE_TEST_CODE" != "400" ] && { echo "ОШИБКА"; exit 1; }
|
||
echo -e "\033[0;32mOK\033[0m"
|
||
else
|
||
exit 1
|
||
fi
|
||
elif [ $ret -ne 0 ]; then
|
||
exit 1
|
||
fi
|
||
else
|
||
echo -e "\033[0;32mOK\033[0m"
|
||
fi
|
||
|
||
_creds_file="$HOME/.claude/.credentials.json"
|
||
_creds_backup=""
|
||
[ -f "$_creds_file" ] && _creds_backup=$(cat "$_creds_file")
|
||
|
||
echo -e "\033[0;33m[ИНФО]\033[0m Режим ChatGPT. Для выхода: Ctrl+C или /exit"
|
||
echo -e " Команда \033[1m/logout\033[0m выйдет из Anthropic, а не из ChatGPT."
|
||
echo ""
|
||
|
||
ANTHROPIC_BASE_URL=http://127.0.0.1:18765 \
|
||
ANTHROPIC_AUTH_TOKEN=dummy \
|
||
ANTHROPIC_MODEL=gpt-5.5 \
|
||
ANTHROPIC_DEFAULT_OPUS_MODEL=gpt-5.5 \
|
||
ANTHROPIC_DEFAULT_SONNET_MODEL=gpt-5.5 \
|
||
ANTHROPIC_DEFAULT_HAIKU_MODEL=gpt-5.4-mini \
|
||
CLAUDE_CODE_SUBAGENT_MODEL=gpt-5.4-mini \
|
||
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 \
|
||
claude "$@"
|
||
|
||
if [ -n "$_creds_backup" ] && [ ! -f "$_creds_file" ]; then
|
||
mkdir -p "$(dirname "$_creds_file")"
|
||
echo "$_creds_backup" > "$_creds_file"
|
||
echo -e "\033[0;33m[ИНФО]\033[0m Anthropic credentials восстановлены."
|
||
fi
|
||
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
|
||
# ============================================================
|
||
# claude_kimi — запуск нативного Kimi Code через Artemox API
|
||
# ============================================================
|
||
|
||
kimi_bin="$HOME/.kimi-code/bin/kimi"
|
||
|
||
if [ ! -f "$kimi_bin" ]; then
|
||
echo "Ошибка: Kimi Code не найден ($kimi_bin)."
|
||
echo "Установите: npm install -g @moonshot-ai/kimi-code"
|
||
exit 1
|
||
fi
|
||
|
||
exec "$kimi_bin" "$@"
|
||
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} — GPT-5.5 (ChatGPT Plus/Pro, браузерная авторизация)"
|
||
echo -e " ${CYAN}claude_deepseek${NC} — DeepSeek (API ключ сохраняется)"
|
||
echo -e " ${CYAN}claude_kimi${NC} — Kimi K2.6 (нативный Kimi Code + Artemox API)"
|
||
echo -e " ${CYAN}claude_gemini${NC} — Gemini (Google OAuth через браузер)"
|
||
echo ""
|
||
echo -e "${YELLOW}⚠️ Для Gemini используйте отдельный Google-аккаунт!${NC}"
|
||
echo ""
|