refactor: migrate claude_gpt and claude_kimi to native CLIs with auto-install
claude_gpt: - Replace proxy orchestration with direct codex binary launch - Auto-install via curl -fsSL https://chatgpt.com/codex/install.sh | sh claude_kimi: - Remove openai-anthropic-proxy.py (~450 lines) - Replace Artemox API key flow with native kimi CLI - Auto-install via curl -fsSL https://code.kimi.com/kimi-code/install.sh | bash tests/test_fixes.sh: - Remove obsolete proxy-specific tests (trap, readiness loops, etc.) - Add tests for auto-install URLs and absence of proxy logic
This commit is contained in:
565
claude_setup.sh
565
claude_setup.sh
@@ -178,464 +178,7 @@ 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) ────────────────────
|
||||
# ── 4c. 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 уже установлен"
|
||||
@@ -869,8 +412,14 @@ 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"
|
||||
echo "OpenAI Codex не найден. Устанавливаю..."
|
||||
curl -fsSL https://chatgpt.com/codex/install.sh | sh
|
||||
codex_bin="$HOME/.npm-global/bin/codex"
|
||||
[ ! -f "$codex_bin" ] && codex_bin="$(command -v codex 2>/dev/null)"
|
||||
fi
|
||||
|
||||
if [ -z "$codex_bin" ] || [ ! -f "$codex_bin" ]; then
|
||||
echo "Ошибка: не удалось установить OpenAI Codex."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -951,92 +500,24 @@ 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
|
||||
# claude_kimi — запуск нативного Kimi Code
|
||||
|
||||
key_file="$HOME/.config/claude-launcher/kimi_key"
|
||||
api_key=""
|
||||
reauth=0
|
||||
kimi_bin="$HOME/.kimi-code/bin/kimi"
|
||||
[ ! -f "$kimi_bin" ] && kimi_bin="$(command -v kimi 2>/dev/null)"
|
||||
|
||||
[ -f "$key_file" ] && api_key=$(cat "$key_file")
|
||||
if [ -z "$kimi_bin" ] || [ ! -f "$kimi_bin" ]; then
|
||||
echo "Kimi Code не найден. Устанавливаю..."
|
||||
curl -fsSL https://code.kimi.com/kimi-code/install.sh | bash
|
||||
kimi_bin="$HOME/.kimi-code/bin/kimi"
|
||||
[ ! -f "$kimi_bin" ] && kimi_bin="$(command -v kimi 2>/dev/null)"
|
||||
fi
|
||||
|
||||
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
|
||||
if [ -z "$kimi_bin" ] || [ ! -f "$kimi_bin" ]; then
|
||||
echo "Ошибка: не удалось установить Kimi Code."
|
||||
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 "$@"
|
||||
exec "$kimi_bin" "$@"
|
||||
KIMIEOF
|
||||
chmod +x "$BIN_DIR/claude_kimi"
|
||||
|
||||
@@ -1125,9 +606,9 @@ 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_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_kimi${NC} — Kimi K2.6 (нативный CLI, автоустановка)"
|
||||
echo -e " ${CYAN}claude_gemini${NC} — Gemini (Google OAuth через браузер)"
|
||||
echo ""
|
||||
echo -e "${YELLOW}⚠️ Для Gemini используйте отдельный Google-аккаунт!${NC}"
|
||||
|
||||
@@ -12,54 +12,42 @@ fail() { echo "[FAIL] $1"; FAIL=$((FAIL+1)); }
|
||||
|
||||
# Extract sections
|
||||
GPT_SECTION=$(awk '/^cat > "\$BIN_DIR\/claude_gpt"/,/^GPTEOF/' "$SCRIPT")
|
||||
KIMI_SECTION=$(awk '/^cat > "\$BIN_DIR\/claude_kimi"/,/^KIMIEOF/' "$SCRIPT")
|
||||
GEMINI_SECTION=$(awk '/^cat > "\$BIN_DIR\/claude_gemini"/,/^GEMINIEOF/' "$SCRIPT")
|
||||
OPENAI_PROXY_SECTION=$(awk '/^cat > "\$OPENAI_ANTHROPIC_PROXY_BIN"/,/^PYEOF/' "$SCRIPT")
|
||||
|
||||
# ── Fix 2: trap EXIT kills proxy ──────────────────────────────────────────────
|
||||
test_fix2_trap_exit() {
|
||||
if echo "$GPT_SECTION" | grep -q "trap .* EXIT"; then
|
||||
ok "Fix2: trap EXIT for proxy cleanup present in claude_gpt"
|
||||
# ── claude_gpt: auto-install codex ────────────────────────────────────────────
|
||||
test_gpt_autoinstall() {
|
||||
if echo "$GPT_SECTION" | grep -q 'curl -fsSL https://chatgpt.com/codex/install.sh'; then
|
||||
ok "claude_gpt: auto-installs codex via official install script"
|
||||
else
|
||||
fail "Fix2: trap EXIT for proxy cleanup missing in claude_gpt"
|
||||
fail "claude_gpt: missing codex auto-install"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Fix 3: readiness loop replaces bare sleep 1 ──────────────────────────────
|
||||
test_fix3_readiness_loop() {
|
||||
if echo "$GPT_SECTION" | grep -q 'while \[ \$_i -lt'; then
|
||||
ok "Fix3: readiness poll loop present in claude_gpt proxy start"
|
||||
# ── claude_gpt: no proxy logic (simplified launcher) ──────────────────────────
|
||||
test_gpt_no_proxy() {
|
||||
if echo "$GPT_SECTION" | grep -q 'ANTHROPIC_BASE_URL'; then
|
||||
fail "claude_gpt: still contains proxy logic (ANTHROPIC_BASE_URL)"
|
||||
else
|
||||
fail "Fix3: readiness poll loop missing in claude_gpt"
|
||||
fi
|
||||
|
||||
if echo "$GPT_SECTION" | grep -qP 'proxy_pid=\$!\n\s+sleep 1\n\s+fi'; then
|
||||
fail "Fix3: bare 'sleep 1' still present right after proxy_pid=\$!"
|
||||
else
|
||||
ok "Fix3: bare 'sleep 1; fi' pattern removed"
|
||||
ok "claude_gpt: proxy logic removed (no ANTHROPIC_BASE_URL)"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Fix 3b: curl exit-7 logic correct ────────────────────────────────────────
|
||||
test_fix3b_exit7_logic() {
|
||||
if echo "$GPT_SECTION" | grep -q 'exit 7 = connection refused'; then
|
||||
ok "Fix3b: exit-7 comment present (connection refused check documented)"
|
||||
# ── claude_kimi: auto-install kimi ────────────────────────────────────────────
|
||||
test_kimi_autoinstall() {
|
||||
if echo "$KIMI_SECTION" | grep -q 'curl -fsSL https://code.kimi.com/kimi-code/install.sh'; then
|
||||
ok "claude_kimi: auto-installs kimi via official install script"
|
||||
else
|
||||
fail "Fix3b: exit-7 comment missing"
|
||||
fi
|
||||
|
||||
if echo "$GPT_SECTION" | grep -q '\[ "\$?" -ne 7 \]'; then
|
||||
ok "Fix3b: [ \$? -ne 7 ] break condition present"
|
||||
else
|
||||
fail "Fix3b: exit-7 break condition missing"
|
||||
fail "claude_kimi: missing kimi auto-install"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Fix 4: re-validate after claude_gpt reauth ───────────────────────────────
|
||||
test_fix4_gpt_revalidate() {
|
||||
if echo "$GPT_SECTION" | grep -q '_claude_test_api.*http://localhost:18765'; then
|
||||
ok "Fix4: _claude_test_api called in claude_gpt"
|
||||
# ── claude_kimi: no proxy logic (simplified launcher) ─────────────────────────
|
||||
test_kimi_no_proxy() {
|
||||
if echo "$KIMI_SECTION" | grep -q 'ANTHROPIC_BASE_URL'; then
|
||||
fail "claude_kimi: still contains proxy logic (ANTHROPIC_BASE_URL)"
|
||||
else
|
||||
fail "Fix4: _claude_test_api missing in claude_gpt"
|
||||
ok "claude_kimi: proxy logic removed (no ANTHROPIC_BASE_URL)"
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -83,15 +71,6 @@ test_fix7_trap_tmp() {
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Kimi/OpenAI proxy: Claude Code sends /v1/messages?beta=true ─────────────
|
||||
test_kimi_proxy_accepts_query_string() {
|
||||
if echo "$OPENAI_PROXY_SECTION" | grep -q 'urlparse(self.path).path'; then
|
||||
ok "Kimi proxy: strips query string before routing /messages"
|
||||
else
|
||||
fail "Kimi proxy: does not handle /v1/messages?beta=true"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── bash syntax of the whole script ─────────────────────────────────────────
|
||||
test_script_syntax() {
|
||||
if bash -n "$SCRIPT" 2>&1; then
|
||||
@@ -103,13 +82,12 @@ test_script_syntax() {
|
||||
|
||||
# ── run all tests ─────────────────────────────────────────────────────────────
|
||||
test_script_syntax
|
||||
test_fix2_trap_exit
|
||||
test_fix3_readiness_loop
|
||||
test_fix3b_exit7_logic
|
||||
test_fix4_gpt_revalidate
|
||||
test_gpt_autoinstall
|
||||
test_gpt_no_proxy
|
||||
test_kimi_autoinstall
|
||||
test_kimi_no_proxy
|
||||
test_fix5_gemini_revalidate
|
||||
test_fix7_trap_tmp
|
||||
test_kimi_proxy_accepts_query_string
|
||||
|
||||
echo ""
|
||||
echo "Results: $PASS passed, $FAIL failed"
|
||||
|
||||
Reference in New Issue
Block a user