Skip to content
This repository was archived by the owner on Oct 8, 2026. It is now read-only.

Commit 0d48ecf

Browse files
rootclaude
andcommitted
release: v1.9.91 CLIProxyAPI sync + pool stats + hard-delete no-SSO
- CLIProxyAPI: settings + push local accounts to CPA auth dir (Management API) - CPA file format import/export (xai/codex + cliproxyapi-auth-bundle) - Pool counts exclusive: live/rotatable excludes cooldown/blocked/expired - Renew fail ×2 without SSO: hard-delete credentials - History compact defaults off (IQ first); soft-tier optional Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 3d9c447 commit 0d48ecf

23 files changed

Lines changed: 2351 additions & 532 deletions

‎.env.example‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,15 @@ GROK2API_SSE_KEEPALIVE=4
5959
# GROK2API_HTTP_READ_TIMEOUT=180
6060
# Overall request timeout
6161
# GROK2API_TIMEOUT=900
62-
# Long tool-loop history compaction (OFF by default).
62+
# Long tool-loop history compaction (OFF by default — IQ first).
63+
# Full tool history is preserved so the model does not re-Read / forget.
64+
# Enable only if huge sessions make upstream fail; soft-tier keeps head+tail.
65+
# GROK2API_HISTORY_COMPACT=1
6366
GROK2API_HISTORY_COMPACT=0
67+
# Auto-force compact above this messages JSON size. 0 = never (IQ-first).
68+
GROK2API_HISTORY_COMPACT_AUTO_CHARS=0
69+
# If compact is ON: keep last N tool rounds nearly fully (default 32).
70+
GROK2API_HISTORY_KEEP_TOOL_ROUNDS=32
6471
# When compaction is on, keep older rewrites byte-stable across turns so
6572
# automatic upstream prompt-prefix cache can hit (same idea as grok-cli).
6673
GROK2API_HISTORY_PREFIX_STABLE=1

‎README.md‎

Lines changed: 31 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,23 @@
22

33
把 **Grok OIDC 登录态** 转成 **OpenAI / Anthropic 兼容 API**,并附带 Web 管理台:多 API Key、多账号轮询、设备码 / SSO / JSON 导入导出、协议注册。
44

5-
**当前版本:v1.9.90** · 修复 Claude Code tool call could not be parsed · 空 schema 工具补 `{}` · stop_reason 对齐
5+
**当前版本:v1.9.91** · CLIProxyAPI 号池同步 · 号池统计修正 · 无 SSO 续期失败硬删 · 历史压缩默认关(智商优先)
66

77
[![GHCR](https://img.shields.io/badge/ghcr.io-hm2899%2Fgrokcli--2api-blue)](https://github.com/users/HM2899/packages/container/package/grokcli-2api)
88
[![Release](https://img.shields.io/github/v/release/HM2899/grokcli-2api?display_name=tag)](https://github.com/HM2899/grokcli-2api/releases)
99

1010
| 镜像(全小写) | 说明 |
1111
|----------------|------|
12-
| `ghcr.io/hm2899/grokcli-2api:1.9.90` | 当前版本 |
12+
| `ghcr.io/hm2899/grokcli-2api:1.9.91` | 当前版本 |
1313
| `ghcr.io/hm2899/grokcli-2api:latest` | 最近 `v*` tag |
1414
| `ghcr.io/hm2899/grokcli-2api:edge` | `main` 最新 |
1515

1616
- **独立运行**:不依赖本地 Grok CLI / 浏览器 OAuth
1717
- **Hybrid 存储(默认强制)**:PostgreSQL 持久 + Redis 热状态 + 多 Worker
1818
- **大账号池轮询**:`round_robin` / `least_used` / `random`;**没额度立即冷却踢出**;pick-time inflight 负载分散
1919
- **会话粘性 / Prompt Cache**:`prompt_cache_key` / Claude session / messages hash;model 隔离绑定;TTL 可热改
20-
- **协议注册**:内置 `grok-build-auth`(纯 HTTP,无需 Chromium);**SSO 入库**;可选 **注册成功后自动推 sub2api**
21-
- **中继友好**:兼容 new-api / sub2api / Claude Code / Codex;`Update`/`StrReplace` → `Edit`;**后到完整参数覆盖错误路径**
20+
- **协议注册**:内置 `grok-build-auth`(纯 HTTP,无需 Chromium);**SSO 入库**;可选 **注册成功后自动推 sub2api / CLIProxyAPI**
21+
- **中继友好**:兼容 new-api / sub2api / CLIProxyAPI / Claude Code / Codex;`Update`/`StrReplace` → `Edit`;**后到完整参数覆盖错误路径**
2222
- **秒开流 + 可观测**:early SSE 信封;用量明细含 `ttft_ms` / `latency_ms` / **思考强度**;任务日志 + 终态帧
2323

2424
---
@@ -51,35 +51,35 @@
5151
| Anthropic 兼容 | `/v1/messages` · tools / tool_use · `count_tokens` |
5252
| Claude Code 工具 | Grok `Update`/`StrReplace` → 客户端 `Edit`;**后到完整参数覆盖错误路径(含 both-complete)**;`target_file` 等别名归一;残缺编辑不下发 |
5353
| 注册机 | 批次自愈 + 孤儿回收;全局 inflight;Device Flow 重试;**SSO 入库 + 文件备份**;导出可走账号库;进度卡防连环 toast |
54-
| 管理台 | 账号、Key、协议注册、测活、续期、任务日志、用量、**系统设置(维护/压缩/探测/sub2api 容量)** |
54+
| 管理台 | 账号、Key、协议注册、测活、续期、任务日志、用量、**系统设置(维护/压缩/探测/sub2api · CLIProxyAPI)** |
5555
| 多账号轮询 | `round_robin` / `least_used` / `random`;**pick-time inflight 分散**;可选**出站代理池** |
5656
| 会话粘性 | `prompt_cache_key` / `previous_response_id` 粘同一账号;**TTL 可热改** |
5757
| 冷却状态 | **没额度立即冷却踢出**(任意轮询策略);live 硬排除;仅测活成功 / 手动解除才回池 |
58-
| Token 过期 / 续期失败 | access token 过期立刻 `pool_status=expired` 移出轮询;连续 2 次 RT 失败 → 有 SSO 则重转,无 SSO 则移出号池 |
59-
| 号池统计 | 首页冷却 / 过期 / 禁用 **直接数库里 `pool_status` 等账号状态字段** |
58+
| Token 过期 / 续期失败 | access token 过期立刻 `pool_status=expired` 移出轮询;连续 2 次 RT 失败 → 有 SSO 则重转,**无 SSO 则硬删账号** |
59+
| 号池统计 | 总量 / 可轮询 / 冷却 / 过期 / 封禁 **互斥分类**(`pool_status` 权威) |
6060
| Token 续期 | 后台 leader 维护;**维护间隔 / 提前刷新窗口可配置** |
6161
| 模型探测 | 单账号 / 多选批量 / 全量;**探测模型列表 / 间隔 / 自动踢出可配置** |
6262
| 协议注册 | MoeMail / YYDS / GPTMail / CF Temp Email + 内联过盾 / YesCaptcha;代理池;入池后延迟测活 |
63-
| SSO / JSON | 后台任务 + 实时进度;JSON 多文件导入 / 选中导出;**一键推送 sub2api(账号容量/优先级/倍率)** |
63+
| SSO / JSON / CPA | 后台任务 + 实时进度;JSON 多文件导入;**一键推送 sub2api**;**一键同步 CLIProxyAPI auth 目录**;CPA/auth 文件双向格式兼容 |
6464
| 任务日志 | 注册、SSO、JSON、测活、续期等结果落 PG |
6565
| 用量统计 | 代理侧 token / 请求:今日·近 N 天·累计;按 Key / 账号 / 模型;**首字 TTFT / 完成耗时 / 思考强度** |
6666
| 流式可靠性 | early SSE 信封;**假阳性 client_gone 不再丢中间 tool/text 帧**;错误/断开仍发终态帧 |
6767
| 容器时区 | 默认 `TZ=Asia/Shanghai`(日志与本地时间) |
6868

6969
---
7070

71-
## 本版本重点(v1.9.90)
71+
## 本版本重点(v1.9.91)
7272

7373
| 能力 | 行为 |
7474
|------|------|
75-
| **Claude Code tool 解析** | 修复 `The model's tool call could not be parsed (retry also failed)` |
76-
| **stop_reason 对齐** | 仅在实际发出 `tool_use` 内容块时返回 `stop_reason=tool_use` |
77-
| **空 schema 工具** | EnterPlanMode / TaskList / ExitPlanMode 等无参数工具补 `input: {}` |
78-
| **不完整参数** | 丢弃残缺 tool arguments,不再下发 `{"_raw":...}` 触发 malformed |
79-
| **流式/非流式一致** | `AnthropicStreamAssembler.finish` 与 `openai_completion_to_anthropic` 同一规则 |
80-
| **包结构重构** | 业务代码迁入 `grok2api/{admin,pool,protocol,upstream,store}`;根目录 / `store/*` 保留兼容 shim |
75+
| **CLIProxyAPI 同步** | 设置页配置 CPA Management API;账号页「导入 CLIProxyAPI(选中/全部)」把本地号池推到 CPA auth 目录 |
76+
| **CPA 文件格式** | 导入识别 `type=xai/codex` 单文件与 `cliproxyapi-auth-bundle`;导出官方 CPA 单账号 JSON 包 |
77+
| **号池统计修正** | `live/可轮询` 不再把冷却/封禁算进去;分类互斥:enabled · cooldown · model_blocked · expired · disabled |
78+
| **无 SSO 续期失败** | 连续 2 次 RT 失败且无 SSO:**硬删凭证 + 号池行**(不再假挂在总量里) |
79+
| **历史压缩默认关** | 默认不压历史(智商优先);可选软分层压缩作安全阀 |
80+
| **注册自动推送** | 可选注册成功后自动推 **sub2api** 与/或 **CLIProxyAPI** |
8181

82-
继承 v1.9.89:使用明细思考强度英文标签 · Token 过期移出轮询 · SSO 续期自愈。
82+
继承 v1.9.90:Claude Code tool 解析修复 · stop_reason 对齐 · `grok2api/` 包布局。
8383

8484
---
8585

@@ -163,7 +163,7 @@ ghcr.io/hm2899/grokcli-2api
163163
**正确示例:**
164164

165165
```bash
166-
docker pull ghcr.io/hm2899/grokcli-2api:1.9.90
166+
docker pull ghcr.io/hm2899/grokcli-2api:1.9.91
167167
# 或
168168
docker pull ghcr.io/hm2899/grokcli-2api:latest
169169
```
@@ -202,7 +202,7 @@ services:
202202
retries: 10
203203

204204
grokcli-2api:
205-
image: ghcr.io/hm2899/grokcli-2api:1.9.90
205+
image: ghcr.io/hm2899/grokcli-2api:1.9.91
206206
ports:
207207
# 只映射应用;不要给 postgres/redis 加 ports
208208
- "3000:3000"
@@ -451,15 +451,17 @@ docker exec grokcli-2api sh -c 'echo TZ=$TZ; date'
451451
```bash
452452
# 1) grok2api/app.py 中 APP_VERSION 必须与 git tag 一致(镜像路径全小写)
453453
# 2) 推 main → edge + 版本号;推 v* tag → 额外 latest + GitHub Release
454-
git add -A && git commit -m "release: v1.9.90"
454+
git add -A && git commit -m "release: v1.9.91"
455455
git push origin main
456-
git tag -a v1.9.90 -m "v1.9.90"
457-
git push origin v1.9.90
458-
gh release create v1.9.90 --title "v1.9.90 Claude Code tool parse + package layout" --notes-file - <<'EOF'
456+
git tag -a v1.9.91 -m "v1.9.91"
457+
git push origin v1.9.91
458+
gh release create v1.9.91 --title "v1.9.91 CLIProxyAPI sync + pool stats + hard-delete no-SSO" --notes-file - <<'EOF'
459459
## Highlights
460-
- 修复 Claude Code `tool call could not be parsed`(空 schema 补 `{}`、stop_reason 对齐、丢弃残缺 tool args)
461-
- 业务代码迁入 `grok2api/` 包;根目录与 `store/*` 保留兼容 shim
462-
- 继承 v1.9.89:使用明细思考强度英文标签
460+
- CLIProxyAPI:设置页配置 + 账号页一键同步本地号池到 CPA auth 目录(Management API)
461+
- CPA 文件格式导入/导出(xai/codex 单文件、cliproxyapi-auth-bundle)
462+
- 号池统计互斥修正:可轮询 ≠ 总量 − 过期
463+
- 续不上 AT 且无 SSO:硬删账号
464+
- 历史压缩默认关闭(智商优先)
463465
EOF
464466
# 监视构建
465467
gh run list --workflow=docker-publish.yml --limit 3
@@ -468,7 +470,7 @@ gh run list --workflow=docker-publish.yml --limit 3
468470
成功后拉取(**必须小写**):
469471

470472
```bash
471-
docker pull ghcr.io/hm2899/grokcli-2api:1.9.90
473+
docker pull ghcr.io/hm2899/grokcli-2api:1.9.91
472474
docker pull ghcr.io/hm2899/grokcli-2api:latest
473475
```
474476

@@ -490,7 +492,7 @@ grok2api/app.py # FastAPI 应用、OpenAI/Anthropic 路
490492
grok2api/admin/ # 管理台路由、API Key、设置、任务日志、用量统计
491493
grok2api/pool/ # 账号池、认证、配额、模型健康、token 维护、会话粘性
492494
grok2api/protocol/ # Claude / Responses / OpenAI 协议兼容与历史压缩
493-
grok2api/upstream/ # Grok 上游、OIDC、sub2api、代理、模型目录
495+
grok2api/upstream/ # Grok 上游、OIDC、sub2api、CLIProxyAPI、代理、模型目录
494496
grok2api/store/ # Redis + PostgreSQL 适配(PG 表、Redis 热状态、锁、指标)
495497
496498
# 兼容层(旧导入仍可用)
@@ -528,7 +530,7 @@ turnstile-solver/ # 本地过盾(内联)
528530

529531
## 版本
530532

531-
- **v1.9.90**(当前)
533+
- **v1.9.91**(当前)
532534
- 修复 Claude Code 报错 `The model's tool call could not be parsed (retry also failed)`
533535
- `stop_reason=tool_use` 仅在实际发出 tool_use 内容块时设置(不再仅因 `finish_reason=tool_calls`)
534536
- 无 required 参数的工具(EnterPlanMode / TaskList / ExitPlanMode 等)空参数补 `input: {}`
@@ -698,7 +700,7 @@ turnstile-solver/ # 本地过盾(内联)
698700
- **v1.9.45–1.9.38**:YYDS 域名、任务日志、JSON/SSO 进度、内联 hybrid 等
699701
- 更早变更见 [GitHub Releases](https://github.com/HM2899/grokcli-2api/releases)
700702

701-
> 镜像 tag 与 `grok2api/app.py` 中 `APP_VERSION` 一致(当前 **1.9.90**)。
703+
> 镜像 tag 与 `grok2api/app.py` 中 `APP_VERSION` 一致(当前 **1.9.91**)。
702704
> 拉取路径固定 **`ghcr.io/hm2899/grokcli-2api`**(全小写)。
703705
704706
## License

‎grok2api/admin/admin_routes.py‎

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -686,11 +686,13 @@ async def admin_status(request: Request):
686686
"mode": pool.get("mode"),
687687
"total": pool.get("total"),
688688
"live": pool.get("live"),
689+
"rotatable": pool.get("rotatable") if pool.get("rotatable") is not None else pool.get("live"),
689690
"enabled": pool.get("enabled"),
690691
"in_cooldown": pool.get("in_cooldown"),
691692
"quota_disabled": pool.get("quota_disabled"),
692693
"model_blocked": pool.get("model_blocked"),
693694
"expired": pool.get("expired"),
695+
"disabled": pool.get("disabled"),
694696
"source": pool.get("source") or "postgres",
695697
},
696698
"keys": key_stats,
@@ -3647,6 +3649,121 @@ async def get_sub2api_settings(
36473649
return {"ok": True, "config": public_sub2api_config()}
36483650

36493651

3652+
@router.get("/settings/cliproxyapi")
3653+
async def get_cliproxyapi_settings(
3654+
request: Request,
3655+
x_admin_token: str | None = Header(default=None, alias="X-Admin-Token"),
3656+
) -> dict[str, Any]:
3657+
"""Return redacted CLIProxyAPI push config for admin UI."""
3658+
require_admin(request, x_admin_token)
3659+
from grok2api.upstream.cliproxyapi_client import public_cliproxyapi_config
3660+
3661+
return {"ok": True, "config": public_cliproxyapi_config()}
3662+
3663+
3664+
class CliproxyapiConfigBody(BaseModel):
3665+
enabled: bool | None = None
3666+
base_url: str | None = None
3667+
management_key: str | None = None
3668+
concurrency: int | None = Field(default=None, ge=1, le=16)
3669+
auto_push_on_register: bool | None = None
3670+
auth_type: str | None = None
3671+
base_upstream: str | None = None
3672+
notes_prefix: str | None = None
3673+
test: bool | None = None
3674+
3675+
3676+
@router.put("/settings/cliproxyapi")
3677+
async def put_cliproxyapi_settings(
3678+
body: CliproxyapiConfigBody,
3679+
request: Request,
3680+
x_admin_token: str | None = Header(default=None, alias="X-Admin-Token"),
3681+
) -> dict[str, Any]:
3682+
"""Save CLIProxyAPI URL + management key. Blank key keeps previous."""
3683+
require_admin(request, x_admin_token)
3684+
from grok2api.upstream.cliproxyapi_client import (
3685+
public_cliproxyapi_config,
3686+
set_cliproxyapi_config,
3687+
test_connection,
3688+
)
3689+
3690+
patch = body.model_dump(exclude_none=True)
3691+
do_test = bool(patch.pop("test", False))
3692+
try:
3693+
set_cliproxyapi_config(patch, replace=False)
3694+
except Exception as e: # noqa: BLE001
3695+
raise HTTPException(status_code=400, detail=str(e)) from e
3696+
out: dict[str, Any] = {"ok": True, "config": public_cliproxyapi_config()}
3697+
if do_test:
3698+
out["test"] = test_connection()
3699+
out["ok"] = bool(out["test"].get("ok"))
3700+
return out
3701+
3702+
3703+
@router.post("/settings/cliproxyapi/test")
3704+
async def test_cliproxyapi_settings(
3705+
request: Request,
3706+
x_admin_token: str | None = Header(default=None, alias="X-Admin-Token"),
3707+
) -> dict[str, Any]:
3708+
"""Login/list smoke test against CPA management API."""
3709+
require_admin(request, x_admin_token)
3710+
from grok2api.upstream.cliproxyapi_client import test_connection
3711+
3712+
result = test_connection()
3713+
return {"ok": bool(result.get("ok")), "test": result}
3714+
3715+
3716+
@router.post("/accounts/push-cliproxyapi")
3717+
async def push_accounts_to_cliproxyapi(
3718+
body: Sub2ApiPushBody,
3719+
request: Request,
3720+
x_admin_token: str | None = Header(default=None, alias="X-Admin-Token"),
3721+
) -> dict[str, Any]:
3722+
"""Batch push local accounts into CLIProxyAPI auth dir (selected or all)."""
3723+
require_admin(request, x_admin_token)
3724+
from grok2api.upstream.cliproxyapi_client import (
3725+
public_cliproxyapi_config,
3726+
push_accounts,
3727+
)
3728+
3729+
cfg = public_cliproxyapi_config()
3730+
if not cfg.get("base_url"):
3731+
raise HTTPException(
3732+
status_code=400,
3733+
detail="请先在设置页填写 CLIProxyAPI URL 与 management key",
3734+
)
3735+
if not cfg.get("has_management_key"):
3736+
raise HTTPException(
3737+
status_code=400,
3738+
detail="请先在设置页填写 CLIProxyAPI management key",
3739+
)
3740+
ids = body.account_ids
3741+
push_all = bool(body.all) or ids is None
3742+
if not push_all and isinstance(ids, list) and len(ids) == 0:
3743+
raise HTTPException(status_code=400, detail="未选择账号")
3744+
try:
3745+
result = push_accounts(
3746+
None if push_all else list(ids or []),
3747+
concurrency=body.concurrency,
3748+
)
3749+
except Exception as e: # noqa: BLE001
3750+
raise HTTPException(status_code=400, detail=str(e)) from e
3751+
audit_log(
3752+
request,
3753+
action="accounts.push_cliproxyapi",
3754+
summary=result.get("message") or "推送账号到 CLIProxyAPI",
3755+
target_type="pool",
3756+
detail={
3757+
"success": result.get("success"),
3758+
"failed": result.get("failed"),
3759+
"total": result.get("total"),
3760+
"all": push_all,
3761+
},
3762+
ok=bool(result.get("ok")),
3763+
)
3764+
return result
3765+
3766+
36503767
class Sub2ApiConfigBody(BaseModel):
36513768
enabled: bool | None = None
36523769
base_url: str | None = None
@@ -3968,6 +4085,62 @@ async def export_sub2api_format(
39684085
return payload
39694086

39704087

4088+
@router.post("/accounts/export-cliproxyapi-format")
4089+
async def export_cliproxyapi_format(
4090+
body: Sub2ApiPushBody,
4091+
request: Request,
4092+
x_admin_token: str | None = Header(default=None, alias="X-Admin-Token"),
4093+
) -> dict[str, Any]:
4094+
"""Export local accounts as CLIProxyAPI auth-bundle JSON.
4095+
4096+
Shape::
4097+
4098+
{
4099+
"type": "cliproxyapi-auth-bundle",
4100+
"version": 1,
4101+
"exported_at": "...",
4102+
"accounts": [
4103+
{
4104+
"type": "xai",
4105+
"email": "...",
4106+
"access_token": "...",
4107+
"refresh_token": "...",
4108+
"expired": "2026-...",
4109+
"base_url": "https://cli-chat-proxy.grok.com/v1",
4110+
"headers": { "X-XAI-Token-Auth": "xai-grok-cli", ... },
4111+
...
4112+
}
4113+
]
4114+
}
4115+
4116+
Each ``accounts[]`` item is a single CPA auth-file body (same as
4117+
``xai-<email>.json`` under CLIProxyAPI's auth dir). Import back via the
4118+
generic JSON 导入 (auto-detects CPA) or drop files into CPA auth dir.
4119+
"""
4120+
require_admin(request, x_admin_token)
4121+
push_all = bool(body.all) or body.account_ids is None
4122+
ids = None if push_all else [str(x).strip() for x in (body.account_ids or []) if str(x).strip()]
4123+
if not push_all and not ids:
4124+
raise HTTPException(status_code=400, detail="未选择账号")
4125+
payload = accounts.export_cliproxyapi_payload(
4126+
account_ids=ids,
4127+
push_all=push_all,
4128+
)
4129+
audit_log(
4130+
request,
4131+
action="accounts.export_cliproxyapi",
4132+
summary=f"导出 CLIProxyAPI 格式 {len(payload.get('accounts') or [])} 个账号",
4133+
target_type="pool",
4134+
detail={
4135+
"count": len(payload.get("accounts") or []),
4136+
"all": push_all,
4137+
"selected": 0 if push_all else len(ids or []),
4138+
},
4139+
ok=True,
4140+
)
4141+
return payload
4142+
4143+
39714144
@router.post("/accounts/logout")
39724145
async def account_logout(
39734146
request: Request,

0 commit comments

Comments
 (0)