{"repo": "2409324124/claude-code-multi-provider-skill", "files": {"HANDOFF.md": {"7bde079cde3ec979a34007308b8843b1813f929d": {"t": 1785595202.2352219, "n": 5896, "h": "2e2c77add66ff9c6", "c": "# 交接文档:Claude Code Multi-Provider Router\n\n## 仓库\nhttps://github.com/2409324124/claude-code-multi-provider-skill\n\n## 当前状态\n路由器已基本可用,但还有几个问题需要修复。\n\n---\n\n## 与 ec7bc57 阶段的区别\n\n### 架构变化\n\n**ec7bc57 阶段**:只有文档和诊断脚本,没有路由器代码。README 描述了一个\"目标架构\",但路由器本身不在仓库里。\n\n**当前版本**:新增了完整的路由器实现 `router/router.py`(~430行)。\n\n### 路由机制对比\n\n| 维度 | ec7bc57(旧) | 当前版本 |\n|------|-------------|---------|\n| **路由器代码** | 不在仓库里,只有文档描述 | `router/router.py`,FastAPI 实现 |\n| **Fallback** | 无(单后端直连) | 多层 fallback chain,每后端可配置顺序 |\n| **错误处理** | 无 | 分类:`retryable`(冷却+fallback)vs `fallback`(仅fallback) |\n| **Cooldown** | 无 | 内存字典,支持 Retry-After 头 |\n| **Stats** | 无 | 每后端 success/fail 计数 |\n| **Streaming** | 未处理 | 先检查 status_code,>=400 走 fallback,2xx 才 passthrough |\n| **Health 端点** | 无 | `GET /` 返回后端状态、统计、冷却、配置 |\n\n### 代码结构\n\n```\nrouter/router.py\n├── BACKENDS # 4个后端配置(从 .env 读取)\n├── ROUTES # 关键词→后端映射表\n├── FALLBACKS # 每后端的 fallback 链\n├── EXTRA_RETRYABLE # 每后端额外可重试状态码(如 GPT 的 400)\n├── _cooldowns # 冷却状态(内存)\n├── _stats # 统计计数(内存)\n├── classify_error() # 错误分类核心函数\n├── proxy_request() # 代理请求(streaming/非streaming)\n├── handle_messages() # 主路由逻辑:遍历候选后端→fallback\n└── health() # GET / 端点\n```\n\n### 路由流程\n\n```\n请求进入 → resolve_backend_name(model) → 得到 primary_name\n→ 构建 candidate_names = [primary] + FALLBACKS[primary]\n→ 遍历候选:\n ├─ 跳过冷却中的\n ├─ rewrite_model(body, backend) # 替换模型名\n ├─ proxy_request(...) # 转发请求\n ├─ classify_error(status, body) # 分类错误\n ├─ retryable → mark_cooldown + continue\n ├─ fallback → continue(不冷却)\n └─ success → return\n→ 全部失败 → 502\n```\n\n---\n\n## 必须修的\n\n### 1. SKILL.md YAML 头部(GitHub 预览报错)\n\n**问题**: GitHub 显示 `Error in user YAML: mapping values are not allowed in this context at line 2 column 93`\n\n**文件**: `claude-code-multi-provider/SKILL.md` 第 1-8 行\n\n**当前内容**:\n```yaml\n---\nname: claude-code-multi-provider\ndescription: \"Configure, debug, and audit a Claude Code multi-provider setup with GPT, DeepSeek, MiMo, Gemini, local tier routing, fallback chains, cc-switch profiles, and read-only diagnostics.\"\n---\n```\n\n**注意**: 刚改成了双引号字符串,但 GitHub 可能还没刷新。如果仍然报错,尝试用单引号或去掉引号用 `>-` 折叠标量。\n\n**验证方法**: 打开 https://github.com/2409324124/claude-code-multi-provider-skill/blob/main/claude-code-multi-provider/SKILL.md 看预览是否正常。\n\n---\n\n### 2. Streaming fallback 逻辑漏洞\n\n**问题**: `router.py` 中 `proxy_request` 的 streaming 分支,如果后端返回 429/500 等错误状态码,会直接包装成 `StreamingResponse` 返回,不会进入 `classify_error` 的 fallback 逻辑。\n\n**文件**: `claude-code-multi-provider/router/router.py`,`proxy_request` 函数,约第 240-273 行\n\n**当前代码**(有问题):\n```python\nbackend_response = await client.send(req, stream=True)\n# ... 异常处理 ...\n\n# 直接包装成 StreamingResponse,没有检查 status_code\nasync def stream_response():\n ...\n\nreturn StreamingResponse(stream_response(), status_code=backend_response.status_code, ...)\n```\n\n**应该改成**:\n```python\nbackend_response = await client.send(req, stream=True)\n# ... 异常处理 ...\n\n# 先检查状态码\nif backend_response.status_code >= 400:\n # 读取错误响应体,关闭连接,返回普通 Response(让调用方能 fallback)\n content = await backend_response.aread()\n await backend_response.aclose()\n await client.aclose()\n return Response(\n content=content,\n status_code=backend_response.status_code,\n media_type=backend_response.headers.get(\"content-type\", \"application/json\").split(\";\")[0],\n headers={\n \"x-route-backend\": backend.get(\"model\", \"\"),\n \"x-route-status\": str(backend_response.status_code),\n },\n )\n\n# 只有 2xx 才真正 passthrough streaming\nasync def stream_response():\n ...\nreturn StreamingResponse(stream_response(), ...)\n```\n\n**同时更新 README 的 Streaming Limitations 章节**,改为:\n> Streaming mode supports fallback **before** the response body is streamed. The router checks the HTTP status code immediately after connecting to the backend. If the backend returns 200, the router commits to streaming and passes chunks through directly.\n\n---\n\n## 建议修的(优先级中等)\n\n### 3. .env.example API key 占位符\n\n**文件**: `claude-code-multi-provider/router/.env.example`\n\n**问题**: `MIMO_API_KEY=your-mimo-api-key-here` 和 `DEEPSEEK_API_KEY=your-deepseek-api-key-here` 是非注释状态,用户直接复制后 router 会把这个假值当真实 key 发出去。\n\n**建议改成**:\n```env\n# MIMO_API_KEY=\n# DEEPSEEK_API_KEY=\n```\n\n或者在 `router.py` 里加一个 key 清洗函数:\n```python\ndef clean_key(value: str | None) -> str | None:\n if not value:\n return None\n if value.startswith(\"your-\") or value.endswith(\"-here\"):\n return None\n return value\n```\n\n---\n\n### 4. README 表格渲染\n\n**问题**: GitHub 预览里表格可能显示不正常(行被压到一起)。\n\n**文件**: `README.md`\n\n**检查**: 打开 GitHub 预览,看 Router Features 表格和 Validated raine models 表格是否正常渲染。如果不行,确保表格前后有空行,且 `|` 对齐。\n\n---\n\n### 5. GitHub 语言检测\n\n**问题**: 仓库主页显示 Shell 100%,但核心是 Python router。\n\n**文件**: 根目录\n\n**建议**: 添加 `.gitattributes`:\n```\n*.py linguist-language=Python\n*.sh linguist-language=Shell\n```\n\n---\n\n## 已完成的(不用再动)\n\n- [x] 关键词路由(opus→GPT, sonnet→DeepSeek, haiku→MiMo, subagent→Gemini)\n- [x] Fallback chains(可配置顺序)\n- [x] 错误分类(retryable vs fallback)\n- [x] 每后端可配置额外可重试状态码(`GPT_EXTRA_RETRYABLE=400`)\n- [x] Cooldown 机制(支持 Retry-After 头)\n- [x] 每后端统计(success/fail/cooldown)\n- [x] 增强 health 端点\n- [x] .env.example 和 pyproject.toml\n- [x] .gitignore(排除 .env)\n- [x] README 和 SKILL.md 基本文档\n\n## 测试命令\n\n```bash\n# 启动路由器\nsetsid python router.py router.log 2>&1 &\n\n# 健康检查\ncurl http://127.0.0.1:8084/\n\n# 路由测试\ncurl -s -X POST http://127.0.0.1:8084/v1/messages -H \"Content-Type: application/json\" -d '{\"model\":\"router/haiku\",\"messages\":[{\"role\":\"user\",\"content\":\"hi\"}],\"max_tokens\":5}'\ncurl -s -X POST http://127.0.0.1:8084/v1/messages -H \"Content-Type: application/json\" -d '{\"model\":\"router/sonnet\",\"messages\":[{\"role\":\"user\",\"content\":\"hi\"}],\"max_tokens\":5}'\ncurl -s -X POST http://127.0.0.1:8084/v1/messages -H \"Content-Type: application/json\" -d '{\"model\":\"router/opus\",\"messages\":[{\"role\":\"user\",\"content\":\"hi\"}],\"max_tokens\":5}'\n\n# Claude Code 真实调用\nclaude -p \"回复一个字:好\"\nclaude -p \"回复一个字:好\" --model haiku\n```\n"}}, "claude-code-multi-provider/scripts/diagnose.sh": {"7bde079cde3ec979a34007308b8843b1813f929d": {"t": 1785595216.9090676, "n": 4183, "h": "cfb69bf7d9ac363d", "c": "#!/usr/bin/env bash\nset -euo pipefail\n\nHEALTH=0\nif [[ \"${1:-}\" == \"--health\" ]]; then\n HEALTH=1\nfi\n\nsection() {\n printf '\\n== %s ==\\n' \"$1\"\n}\n\ncmd_status() {\n local cmd=\"$1\"\n if command -v \"$cmd\" >/dev/null 2>&1; then\n printf '%-18s %s\\n' \"$cmd\" \"$(command -v \"$cmd\")\"\n else\n printf '%-18s missing\\n' \"$cmd\"\n fi\n}\n\nmask_json_file() {\n local file=\"$1\"\n if [[ ! -f \"$file\" ]]; then\n echo \"missing: $file\"\n return\n fi\n\n node - \"$file\" <<'NODE'\nconst fs = require('fs');\nconst path = process.argv[2];\nconst secretPattern = /(key|token|secret|refresh|access|credential|password)/i;\n\nfunction mask(value, key = '') {\n if (value === null || value === undefined) return value;\n if (typeof value === 'string') {\n if (secretPattern.test(key)) return `*** len=${value.length}`;\n if (value.startsWith('sk-') || value.startsWith('tp-')) return `*** len=${value.length}`;\n return value;\n }\n if (Array.isArray(value)) return value.map((item) => mask(item, key));\n if (typeof value === 'object') {\n const out = {};\n for (const [k, v] of Object.entries(value)) out[k] = mask(v, k);\n return out;\n }\n return value;\n}\n\ntry {\n const parsed = JSON.parse(fs.readFileSync(path, 'utf8'));\n console.log(JSON.stringify(mask(parsed), null, 2));\n} catch (error) {\n console.log(`unreadable json: ${path}: ${error.message}`);\n}\nNODE\n}\n\nhealth_check() {\n local name=\"$1\"\n shift\n local log\n log=\"$(mktemp)\"\n if timeout 120 \"$@\" -p '只回复 OK' >\"$log\" 2>&1 && rg -q '^OK$|OK' \"$log\"; then\n printf '%-18s OK\\n' \"$name\"\n else\n printf '%-18s FAIL\\n' \"$name\"\n tail -n 20 \"$log\" || true\n fi\n rm -f \"$log\"\n}\n\nsection \"Commands\"\nfor cmd in claude cc-switch raine-claude-code-proxy clawgate claude-router claude-gpt claude-gemini claude-auto uv gcloud gemini ss rg node; do\n cmd_status \"$cmd\"\ndone\n\nsection \"Listening Ports\"\nif command -v ss >/dev/null 2>&1; then\n ss -ltnp 2>/dev/null | rg '127\\.0\\.0\\.1:18765|127\\.0\\.0\\.1:8082|127\\.0\\.0\\.1:8083|127\\.0\\.0\\.1:8084|raine-claude|clawgate|uvicorn' || true\nelse\n echo \"ss missing\"\nfi\n\nsection \"Current cc-switch Status\"\nif command -v cc-switch >/dev/null 2>&1; then\n cc-switch status || true\nelse\n echo \"cc-switch missing\"\nfi\n\nsection \"Claude Settings (masked)\"\nmask_json_file \"$HOME/.claude/settings.json\"\n\nsection \"cc-switch Profiles (masked)\"\nmask_json_file \"$HOME/.cc-switch/profiles.json\"\n\nsection \"Provider Files\"\necho \"Auth boundary: GPT and Gemini are normally authenticated by local proxy credential files; DeepSeek and MiMo normally use API keys in the router .env.\"\nfor file in \\\n \"$HOME/.clawgate/token.json\" \\\n \"$HOME/.config/claude-code-proxy/codex/auth.json\" \\\n \"$HOME/tools/raine-claude-code-proxy/proxy.log\" \\\n \"$HOME/tools/claude-code-proxy-gemini/.env\" \\\n \"$GOOGLE_APPLICATION_CREDENTIALS\" \\\n \"$HOME/.config/gcloud/application_default_credentials.json\" \\\n \"$HOME/.gemini/oauth_creds.json\"; do\n if [[ -n \"${file:-}\" && -f \"$file\" ]]; then\n printf 'present %s mode=%s\\n' \"$file\" \"$(stat -c '%a' \"$file\" 2>/dev/null || echo unknown)\"\n elif [[ -n \"${file:-}\" ]]; then\n printf 'missing %s\\n' \"$file\"\n fi\ndone\n\nsection \"Router API Key Expectations\"\ncat <<'EOF'\nGPT_API_KEY usually unset; raine proxy handles Codex/ChatGPT auth\nGEMINI_API_KEY usually unset; Gemini proxy handles Google/Vertex auth\nDEEPSEEK_API_KEY required for direct DeepSeek official endpoint\nMIMO_API_KEY required for direct MiMo Token Plan endpoint\nEOF\n\nif [[ \"$HEALTH\" -eq 1 ]]; then\n section \"Health Checks\"\n if command -v claude-router >/dev/null 2>&1; then\n health_check \"claude-router\" claude-router\n else\n echo \"claude-router missing\"\n fi\n\n if command -v claude-gpt >/dev/null 2>&1; then\n health_check \"claude-gpt\" claude-gpt\n else\n echo \"claude-gpt missing\"\n fi\n\n if command -v claude-gemini >/dev/null 2>&1; then\n health_check \"claude-gemini\" claude-gemini\n else\n echo \"claude-gemini missing\"\n fi\n\n echo \"MiMo and DeepSeek health checks are skipped because they require cc-switch use and mutate the active provider.\"\nelse\n section \"Health Checks\"\n echo \"Skipped. Re-run with --health to check claude-router, claude-gpt, and claude-gemini when present.\"\nfi\n"}, "bb9ff06631d8b5fd365a1553110c4de194e4cf3c": {"t": 1785758227.7790692, "n": 4183, "h": "cfb69bf7d9ac363d", "c": "#!/usr/bin/env bash\nset -euo pipefail\n\nHEALTH=0\nif [[ \"${1:-}\" == \"--health\" ]]; then\n HEALTH=1\nfi\n\nsection() {\n printf '\\n== %s ==\\n' \"$1\"\n}\n\ncmd_status() {\n local cmd=\"$1\"\n if command -v \"$cmd\" >/dev/null 2>&1; then\n printf '%-18s %s\\n' \"$cmd\" \"$(command -v \"$cmd\")\"\n else\n printf '%-18s missing\\n' \"$cmd\"\n fi\n}\n\nmask_json_file() {\n local file=\"$1\"\n if [[ ! -f \"$file\" ]]; then\n echo \"missing: $file\"\n return\n fi\n\n node - \"$file\" <<'NODE'\nconst fs = require('fs');\nconst path = process.argv[2];\nconst secretPattern = /(key|token|secret|refresh|access|credential|password)/i;\n\nfunction mask(value, key = '') {\n if (value === null || value === undefined) return value;\n if (typeof value === 'string') {\n if (secretPattern.test(key)) return `*** len=${value.length}`;\n if (value.startsWith('sk-') || value.startsWith('tp-')) return `*** len=${value.length}`;\n return value;\n }\n if (Array.isArray(value)) return value.map((item) => mask(item, key));\n if (typeof value === 'object') {\n const out = {};\n for (const [k, v] of Object.entries(value)) out[k] = mask(v, k);\n return out;\n }\n return value;\n}\n\ntry {\n const parsed = JSON.parse(fs.readFileSync(path, 'utf8'));\n console.log(JSON.stringify(mask(parsed), null, 2));\n} catch (error) {\n console.log(`unreadable json: ${path}: ${error.message}`);\n}\nNODE\n}\n\nhealth_check() {\n local name=\"$1\"\n shift\n local log\n log=\"$(mktemp)\"\n if timeout 120 \"$@\" -p '只回复 OK' >\"$log\" 2>&1 && rg -q '^OK$|OK' \"$log\"; then\n printf '%-18s OK\\n' \"$name\"\n else\n printf '%-18s FAIL\\n' \"$name\"\n tail -n 20 \"$log\" || true\n fi\n rm -f \"$log\"\n}\n\nsection \"Commands\"\nfor cmd in claude cc-switch raine-claude-code-proxy clawgate claude-router claude-gpt claude-gemini claude-auto uv gcloud gemini ss rg node; do\n cmd_status \"$cmd\"\ndone\n\nsection \"Listening Ports\"\nif command -v ss >/dev/null 2>&1; then\n ss -ltnp 2>/dev/null | rg '127\\.0\\.0\\.1:18765|127\\.0\\.0\\.1:8082|127\\.0\\.0\\.1:8083|127\\.0\\.0\\.1:8084|raine-claude|clawgate|uvicorn' || true\nelse\n echo \"ss missing\"\nfi\n\nsection \"Current cc-switch Status\"\nif command -v cc-switch >/dev/null 2>&1; then\n cc-switch status || true\nelse\n echo \"cc-switch missing\"\nfi\n\nsection \"Claude Settings (masked)\"\nmask_json_file \"$HOME/.claude/settings.json\"\n\nsection \"cc-switch Profiles (masked)\"\nmask_json_file \"$HOME/.cc-switch/profiles.json\"\n\nsection \"Provider Files\"\necho \"Auth boundary: GPT and Gemini are normally authenticated by local proxy credential files; DeepSeek and MiMo normally use API keys in the router .env.\"\nfor file in \\\n \"$HOME/.clawgate/token.json\" \\\n \"$HOME/.config/claude-code-proxy/codex/auth.json\" \\\n \"$HOME/tools/raine-claude-code-proxy/proxy.log\" \\\n \"$HOME/tools/claude-code-proxy-gemini/.env\" \\\n \"$GOOGLE_APPLICATION_CREDENTIALS\" \\\n \"$HOME/.config/gcloud/application_default_credentials.json\" \\\n \"$HOME/.gemini/oauth_creds.json\"; do\n if [[ -n \"${file:-}\" && -f \"$file\" ]]; then\n printf 'present %s mode=%s\\n' \"$file\" \"$(stat -c '%a' \"$file\" 2>/dev/null || echo unknown)\"\n elif [[ -n \"${file:-}\" ]]; then\n printf 'missing %s\\n' \"$file\"\n fi\ndone\n\nsection \"Router API Key Expectations\"\ncat <<'EOF'\nGPT_API_KEY usually unset; raine proxy handles Codex/ChatGPT auth\nGEMINI_API_KEY usually unset; Gemini proxy handles Google/Vertex auth\nDEEPSEEK_API_KEY required for direct DeepSeek official endpoint\nMIMO_API_KEY required for direct MiMo Token Plan endpoint\nEOF\n\nif [[ \"$HEALTH\" -eq 1 ]]; then\n section \"Health Checks\"\n if command -v claude-router >/dev/null 2>&1; then\n health_check \"claude-router\" claude-router\n else\n echo \"claude-router missing\"\n fi\n\n if command -v claude-gpt >/dev/null 2>&1; then\n health_check \"claude-gpt\" claude-gpt\n else\n echo \"claude-gpt missing\"\n fi\n\n if command -v claude-gemini >/dev/null 2>&1; then\n health_check \"claude-gemini\" claude-gemini\n else\n echo \"claude-gemini missing\"\n fi\n\n echo \"MiMo and DeepSeek health checks are skipped because they require cc-switch use and mutate the active provider.\"\nelse\n section \"Health Checks\"\n echo \"Skipped. Re-run with --health to check claude-router, claude-gpt, and claude-gemini when present.\"\nfi\n"}, "ec7bc57a7b23e1d1de3c4a5dfb3dc42dff7de9ec": {"t": 1785758229.0125868, "n": 3698, "h": "b811f17ed20f6cff", "c": "#!/usr/bin/env bash\nset -euo pipefail\n\nHEALTH=0\nif [[ \"${1:-}\" == \"--health\" ]]; then\n HEALTH=1\nfi\n\nsection() {\n printf '\\n== %s ==\\n' \"$1\"\n}\n\ncmd_status() {\n local cmd=\"$1\"\n if command -v \"$cmd\" >/dev/null 2>&1; then\n printf '%-18s %s\\n' \"$cmd\" \"$(command -v \"$cmd\")\"\n else\n printf '%-18s missing\\n' \"$cmd\"\n fi\n}\n\nmask_json_file() {\n local file=\"$1\"\n if [[ ! -f \"$file\" ]]; then\n echo \"missing: $file\"\n return\n fi\n\n node - \"$file\" <<'NODE'\nconst fs = require('fs');\nconst path = process.argv[2];\nconst secretPattern = /(key|token|secret|refresh|access|credential|password)/i;\n\nfunction mask(value, key = '') {\n if (value === null || value === undefined) return value;\n if (typeof value === 'string') {\n if (secretPattern.test(key)) return `*** len=${value.length}`;\n if (value.startsWith('sk-') || value.startsWith('tp-')) return `*** len=${value.length}`;\n return value;\n }\n if (Array.isArray(value)) return value.map((item) => mask(item, key));\n if (typeof value === 'object') {\n const out = {};\n for (const [k, v] of Object.entries(value)) out[k] = mask(v, k);\n return out;\n }\n return value;\n}\n\ntry {\n const parsed = JSON.parse(fs.readFileSync(path, 'utf8'));\n console.log(JSON.stringify(mask(parsed), null, 2));\n} catch (error) {\n console.log(`unreadable json: ${path}: ${error.message}`);\n}\nNODE\n}\n\nhealth_check() {\n local name=\"$1\"\n shift\n local log\n log=\"$(mktemp)\"\n if timeout 120 \"$@\" -p '只回复 OK' >\"$log\" 2>&1 && rg -q '^OK$|OK' \"$log\"; then\n printf '%-18s OK\\n' \"$name\"\n else\n printf '%-18s FAIL\\n' \"$name\"\n tail -n 20 \"$log\" || true\n fi\n rm -f \"$log\"\n}\n\nsection \"Commands\"\nfor cmd in claude cc-switch raine-claude-code-proxy clawgate claude-router claude-gpt claude-gemini claude-auto uv gcloud gemini ss rg node; do\n cmd_status \"$cmd\"\ndone\n\nsection \"Listening Ports\"\nif command -v ss >/dev/null 2>&1; then\n ss -ltnp 2>/dev/null | rg '127\\.0\\.0\\.1:18765|127\\.0\\.0\\.1:8082|127\\.0\\.0\\.1:8083|127\\.0\\.0\\.1:8084|raine-claude|clawgate|uvicorn' || true\nelse\n echo \"ss missing\"\nfi\n\nsection \"Current cc-switch Status\"\nif command -v cc-switch >/dev/null 2>&1; then\n cc-switch status || true\nelse\n echo \"cc-switch missing\"\nfi\n\nsection \"Claude Settings (masked)\"\nmask_json_file \"$HOME/.claude/settings.json\"\n\nsection \"cc-switch Profiles (masked)\"\nmask_json_file \"$HOME/.cc-switch/profiles.json\"\n\nsection \"Provider Files\"\nfor file in \\\n \"$HOME/.clawgate/token.json\" \\\n \"$HOME/.config/claude-code-proxy/codex/auth.json\" \\\n \"$HOME/tools/raine-claude-code-proxy/proxy.log\" \\\n \"$HOME/tools/claude-code-proxy-gemini/.env\" \\\n \"$GOOGLE_APPLICATION_CREDENTIALS\" \\\n \"$HOME/.config/gcloud/application_default_credentials.json\" \\\n \"$HOME/.gemini/oauth_creds.json\"; do\n if [[ -n \"${file:-}\" && -f \"$file\" ]]; then\n printf 'present %s mode=%s\\n' \"$file\" \"$(stat -c '%a' \"$file\" 2>/dev/null || echo unknown)\"\n elif [[ -n \"${file:-}\" ]]; then\n printf 'missing %s\\n' \"$file\"\n fi\ndone\n\nif [[ \"$HEALTH\" -eq 1 ]]; then\n section \"Health Checks\"\n if command -v claude-router >/dev/null 2>&1; then\n health_check \"claude-router\" claude-router\n else\n echo \"claude-router missing\"\n fi\n\n if command -v claude-gpt >/dev/null 2>&1; then\n health_check \"claude-gpt\" claude-gpt\n else\n echo \"claude-gpt missing\"\n fi\n\n if command -v claude-gemini >/dev/null 2>&1; then\n health_check \"claude-gemini\" claude-gemini\n else\n echo \"claude-gemini missing\"\n fi\n\n echo \"MiMo and DeepSeek health checks are skipped because they require cc-switch use and mutate the active provider.\"\nelse\n section \"Health Checks\"\n echo \"Skipped. Re-run with --health to check claude-router, claude-gpt, and claude-gemini when present.\"\nfi\n"}, "3e98e2f469aebfbd50700871f726d93baa54179c": {"t": 1785758230.5995393, "n": 3335, "h": "9968c8c8c2075f45", "c": "#!/usr/bin/env bash\nset -euo pipefail\n\nHEALTH=0\nif [[ \"${1:-}\" == \"--health\" ]]; then\n HEALTH=1\nfi\n\nsection() {\n printf '\\n== %s ==\\n' \"$1\"\n}\n\ncmd_status() {\n local cmd=\"$1\"\n if command -v \"$cmd\" >/dev/null 2>&1; then\n printf '%-18s %s\\n' \"$cmd\" \"$(command -v \"$cmd\")\"\n else\n printf '%-18s missing\\n' \"$cmd\"\n fi\n}\n\nmask_json_file() {\n local file=\"$1\"\n if [[ ! -f \"$file\" ]]; then\n echo \"missing: $file\"\n return\n fi\n\n node - \"$file\" <<'NODE'\nconst fs = require('fs');\nconst path = process.argv[2];\nconst secretPattern = /(key|token|secret|refresh|access|credential|password)/i;\n\nfunction mask(value, key = '') {\n if (value === null || value === undefined) return value;\n if (typeof value === 'string') {\n if (secretPattern.test(key)) return `*** len=${value.length}`;\n if (value.startsWith('sk-') || value.startsWith('tp-')) return `*** len=${value.length}`;\n return value;\n }\n if (Array.isArray(value)) return value.map((item) => mask(item, key));\n if (typeof value === 'object') {\n const out = {};\n for (const [k, v] of Object.entries(value)) out[k] = mask(v, k);\n return out;\n }\n return value;\n}\n\ntry {\n const parsed = JSON.parse(fs.readFileSync(path, 'utf8'));\n console.log(JSON.stringify(mask(parsed), null, 2));\n} catch (error) {\n console.log(`unreadable json: ${path}: ${error.message}`);\n}\nNODE\n}\n\nhealth_check() {\n local name=\"$1\"\n shift\n local log\n log=\"$(mktemp)\"\n if timeout 120 \"$@\" -p '只回复 OK' >\"$log\" 2>&1 && rg -q '^OK$|OK' \"$log\"; then\n printf '%-18s OK\\n' \"$name\"\n else\n printf '%-18s FAIL\\n' \"$name\"\n tail -n 20 \"$log\" || true\n fi\n rm -f \"$log\"\n}\n\nsection \"Commands\"\nfor cmd in claude cc-switch clawgate claude-gpt claude-gemini claude-auto uv gcloud gemini ss rg node; do\n cmd_status \"$cmd\"\ndone\n\nsection \"Listening Ports\"\nif command -v ss >/dev/null 2>&1; then\n ss -ltnp 2>/dev/null | rg '127\\.0\\.0\\.1:8082|127\\.0\\.0\\.1:8083|clawgate|uvicorn' || true\nelse\n echo \"ss missing\"\nfi\n\nsection \"Current cc-switch Status\"\nif command -v cc-switch >/dev/null 2>&1; then\n cc-switch status || true\nelse\n echo \"cc-switch missing\"\nfi\n\nsection \"Claude Settings (masked)\"\nmask_json_file \"$HOME/.claude/settings.json\"\n\nsection \"cc-switch Profiles (masked)\"\nmask_json_file \"$HOME/.cc-switch/profiles.json\"\n\nsection \"Provider Files\"\nfor file in \\\n \"$HOME/.clawgate/token.json\" \\\n \"$HOME/tools/claude-code-proxy-gemini/.env\" \\\n \"$GOOGLE_APPLICATION_CREDENTIALS\" \\\n \"$HOME/.config/gcloud/application_default_credentials.json\" \\\n \"$HOME/.gemini/oauth_creds.json\"; do\n if [[ -n \"${file:-}\" && -f \"$file\" ]]; then\n printf 'present %s mode=%s\\n' \"$file\" \"$(stat -c '%a' \"$file\" 2>/dev/null || echo unknown)\"\n elif [[ -n \"${file:-}\" ]]; then\n printf 'missing %s\\n' \"$file\"\n fi\ndone\n\nif [[ \"$HEALTH\" -eq 1 ]]; then\n section \"Health Checks\"\n if command -v claude-gpt >/dev/null 2>&1; then\n health_check \"claude-gpt\" claude-gpt\n else\n echo \"claude-gpt missing\"\n fi\n\n if command -v claude-gemini >/dev/null 2>&1; then\n health_check \"claude-gemini\" claude-gemini\n else\n echo \"claude-gemini missing\"\n fi\n\n echo \"MiMo and DeepSeek health checks are skipped because they require cc-switch use and mutate the active provider.\"\nelse\n section \"Health Checks\"\n echo \"Skipped. Re-run with --health to check claude-gpt and claude-gemini only.\"\nfi\n"}}}}