feat: migrate Kimi from Moonshot to Artemox OpenAI API

- 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
This commit is contained in:
2026-05-31 17:55:57 +07:00
parent a1bf8023a9
commit 42546b4dc1
4 changed files with 548 additions and 65 deletions

View File

@@ -27,7 +27,7 @@ source ~/.bashrc
* `claude_gpt`: Доступ к GPT-5.5 (требует авторизации через `claude-code-proxy`).
* `claude_deepseek`: Доступ к DeepSeek (требуется API ключ).
* `claude_kimi`: Доступ к Kimi K2.6 от Moonshot AI (требуется API ключ).
* `claude_kimi`: Доступ к Kimi K2.6 через Artemox OpenAI-compatible API (требуется API ключ).
* `claude_gemini`: Доступ к Gemini (требует авторизации через веб-интерфейс `antigravity-claude-proxy`).
## Важные замечания

View File

@@ -178,6 +178,463 @@ 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
@@ -255,6 +712,60 @@ _claude_test_api() {
_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 "
@@ -389,7 +900,8 @@ sleep 0.3
if ! pgrep -f "claude-code-proxy serve" &>/dev/null; then
PORT=18766 "$proxy_bin" serve &>/tmp/claude-code-proxy.log &
proxy_pid=$!
_j=0; while [ $_j -lt 10 ]; do sleep 1; curl -s --max-time 1 http://127.0.0.1:18766/ &>/dev/null; [ "$?" -ne 7 ] && break; _j=$((_j + 1)); done
# 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
@@ -399,7 +911,7 @@ 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://127.0.0.1:18765/ &>/dev/null; [ "$?" -ne 7 ] && break; _i=$((_i + 1)); done
_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:"
@@ -408,7 +920,7 @@ if [ $_i -ge 10 ]; then
fi
echo -n "Проверка авторизации ChatGPT... "
_claude_test_api "http://127.0.0.1:18765/v1/messages" "x-api-key: dummy" "gpt-5.4-mini"
_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" ""
@@ -417,7 +929,7 @@ if [ "$_CLAUDE_TEST_CODE" != "400" ]; 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://127.0.0.1:18765/v1/messages" "x-api-key: dummy" "gpt-5.4-mini"
_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
@@ -529,69 +1041,19 @@ 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 через Artemox API
# ============================================================
key_file="$HOME/.config/claude-launcher/kimi_key"
api_key=""
reauth=0
kimi_bin="$HOME/.kimi-code/bin/kimi"
[ -f "$key_file" ] && api_key=$(cat "$key_file")
if [ -n "$api_key" ]; then
echo -n "Проверка сохранённого Kimi ключа... "
_claude_test_api "https://api.moonshot.ai/anthropic/v1/messages" "x-api-key: $api_key" "kimi-k2.6"
_handle_api_response "Kimi" "$_CLAUDE_TEST_CODE" "$_CLAUDE_TEST_BODY" "Пополните баланс: https://platform.moonshot.ai/console/billing"
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
if [ ! -f "$kimi_bin" ]; then
echo "Ошибка: Kimi Code не найден ($kimi_bin)."
echo "Установите: npm install -g @moonshot-ai/kimi-code"
exit 1
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://platform.moonshot.ai/console/api-keys"
read -r -p "Введите ваш Kimi API ключ: " api_key
[ -z "$api_key" ] && { echo "Выход."; exit 1; }
echo -n "Проверяю ключ и баланс... "
_claude_test_api "https://api.moonshot.ai/anthropic/v1/messages" "x-api-key: $api_key" "kimi-k2.6"
_handle_api_response "Kimi" "$_CLAUDE_TEST_CODE" "$_CLAUDE_TEST_BODY" "Пополните баланс: https://platform.moonshot.ai/console/billing"
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.moonshot.ai/anthropic \
ANTHROPIC_AUTH_TOKEN="$api_key" \
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"
@@ -682,7 +1144,7 @@ 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 (Moonshot AI, 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}"

10
test_sigint.sh Executable file
View File

@@ -0,0 +1,10 @@
#!/bin/bash
python3 -c "import time; print('python started'); time.sleep(100)" &
py_pid=$!
node -e "
console.log('node started');
process.on('SIGINT', () => console.log('node caught sigint'));
setTimeout(() => console.log('node done'), 10000);
"
echo "checking python"
kill -0 $py_pid 2>/dev/null && echo "python alive" || echo "python dead"

View File

@@ -13,6 +13,7 @@ fail() { echo "[FAIL] $1"; FAIL=$((FAIL+1)); }
# Extract sections
GPT_SECTION=$(awk '/^cat > "\$BIN_DIR\/claude_gpt"/,/^GPTEOF/' "$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() {
@@ -82,6 +83,15 @@ 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
@@ -99,6 +109,7 @@ test_fix3b_exit7_logic
test_fix4_gpt_revalidate
test_fix5_gemini_revalidate
test_fix7_trap_tmp
test_kimi_proxy_accepts_query_string
echo ""
echo "Results: $PASS passed, $FAIL failed"