Skip to content

Commit 1f8729f

Browse files
committed
fix(codearts): 修流式解析——v2 实测为标准 OpenAI chunk(非累计全文)
实测(2026-09-30 抓真实流)`POST /api/v2/chat/completions` 返回的是标准 OpenAI chunk:`{"choices":[{"delta":{"content","reasoning_content"}, "finish_reason"}]}` + `data:[DONE]`,增量在 delta;逆向记录里的「累计全文 `text`」只适用于旧形状 / legacy `/v1/chat/chat`。旧解析器把 `data:[DONE]` 当 JSON 解析,直接抛 `unparsable CodeArts SSE data`,导致 Playground/客户端 一请求就 500。 - `parse_line`:识别 `[DONE]` 哨兵 → FINISH;按字段分派 OpenAI chunk 与 累计全文两种形状。 - OpenAI chunk:`choices[].delta` 取 content/reasoning_content/tool_calls, `finish_reason` 产 FINISH,`usage`(收尾帧 choices 为空时单独给)产 USAGE。 - 旧形状(累计 `text` / 顶层 `delta` / 平铺 token)保持兼容。 - 测试覆盖两种形状 + `[DONE]` + 空 choices 的 usage 帧。 - 文档同步(PROPOSAL/TECHNICAL)。 已用真实凭证端到端验证:非流式与流式均正常返回,usage 落库 ok=1。
1 parent 03b8eb2 commit 1f8729f

4 files changed

Lines changed: 154 additions & 35 deletions

File tree

‎PROPOSAL.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@
5353
| Q45 | 聊天节流按凭证分桶并允许桶内并发(同渠道同模型并发不再串行台阶) | Q44 给 zen 拆了独立 pacer 后,CB/TRAE 的 `chat_pacer` 仍是**一把全局 `asyncio.Lock` + 单个 `_last_started`**:任何两个请求(哪怕不同账号、不同模型)都串行排队,后到者按 `interval - elapsed` 补足等待。实测 3 个并发 CB 请求 TTFB ≈ 1.55 / 6.71 / 11.47s(正好 +5s、+10s 台阶);把间隔热更为 0 后 6 并发 TTFB ≈ 1.48–1.84s、总 1.84s → 延迟完全来自节流排队而非上游。**关键**:并发请求常被会话粘性/健康度排序收敛到**同一个凭证**(DB 里 6 条并发全部命中 `cred_75e8edcf`),所以只按凭证分桶、桶内继续排队并不能解决,必须同时允许桶内并发。改为 `Pacer(min, max, *, allow_concurrent=False)`:`allow_concurrent=True`(仅聊天 pacer)时按桶(渠道前缀 + 凭证身份摘要)维护**在途计数**——同桶已有在途请求则新请求**立即放行**,只有桶空闲、且距上次请求开始不足最小间隔时才补足等待(即只有「上一请求已结束、紧接着又来一个」的顺序连发才节流)。请求结束由 provider `stream_chat` 的 `finally` 调 `pacer.release(key)` 归还名额(async generator 被提前关闭时依赖 asyncio 的 asyncgen finalize,延迟归还只会让节流略松、不会误排队)。`allow_concurrent=False`(后台任务 pacer)保持原严格串行语义不变。桶键用 `stable_key(provider, identity)`:CB 取 `account_uid or user_id or bearer_token`、TRAE 取 `uid or access_token`、zen 用渠道常量;`identity` 缺失回落该渠道单桶。CB/TRAE 仍共享同一 pacer 实例,但桶键带渠道前缀 + 身份摘要,彼此不互堵;`CODEBUDDY_CHAT_MIN_INTERVAL` 语义从「跨渠道全局间隔」变为「同渠道同凭证的顺序连发间隔」(默认 5s 不变)。无 schema 变更 |
5454
| Q46 | Kilo Gateway 免费层(第四渠道 `kilo`) | 把 [Kilo Gateway](https://kilo.ai)(`api.kilo.ai/api/gateway`)免费层接成第四个 provider(`KNOWN_PROVIDERS` 加 `"kilo"`,**无 schema 变更**)。**协议是标准 OpenAI 兼容**(`/chat/completions` + `/models`),既无私有信封也无门禁伪装——与 Zen 的关键差异正在此:Zen 要伪造 UA/session/tools 并过滤伪工具调用,Kilo 完全不需要。**免费模型有权威标记**:`/models` 每个条目带 `isFree` 布尔(实测 2026-09-29 共 395 个模型、17 个 `isFree=true`,含 `kilo-auto/free`、`stealth/space-bunny-alpha`、`openrouter/free` 等无 `:free` 后缀者),据此**直接过滤**免费集——**不做探活**(与 Zen 相反):探活会真发一次推理、白耗本就极小的免费配额(网关级约 200 req/h/IP),且结果随上游免费池波动不稳定,`isFree` 已足够权威。免费模型显式标 **x0 倍率**(`credit_rate=0.0`);`name`/`context_length`/`top_provider.max_completion_tokens`/`supported_parameters`(含 `tools`)/`architecture.input_modalities`(含 `image`)透传为中立 `Model` 元数据。思考字段是 **`delta.reasoning`**(**不是** Zen 的 `reasoning_content`)。**凭证模型用虚拟凭证行**(同 Zen):Kilo 无凭证/无额度接口,池里种一条空凭证复用现有调度/冷却/统计(`probe_quota` 恒 `probe_failed=True` → health NULL「未知」,**不是耗尽**);删除后重启复活,也可在凭证页「登录渠道账号」点「添加 Kilo Gateway」立即补回,永久停用请用「暂停」。**错误分类**:401→`INVALID`(无凭证,401 只表示该模型需要付费 key/BYOK,避免强制付费模型硬禁用整条渠道)、400/404/422→`INVALID`、403→`REQUEST`、**429 与 502/503/504→`MODEL`(模型级冷却)**。429 与上游 5xx 归模型级而非账号级:实测(2026-09-30)429 报错点名具体模型(`<model> is temporarily rate-limited upstream`,`limit_source: upstream_provider_shared_pool`),429 消退后同一模型转 503 `no endpoints available`,两种情况下**同一时刻其他免费模型仍 200**——免费池实为 OpenRouter 共享池转发,拥塞/端点缺失按模型隔离,归账号级会因单模型问题把整条 kilo 渠道冷却(429→60s;5xx 累计 3 次→10m;单虚拟凭证下均即 `all credentials unavailable`)。对齐 CB/TRAE 的 `429+6004 → MODEL` 口径。限流交引擎处理,**本包不自建熔断**。新增 `KILO_API_ENDPOINT` / `KILO_ALLOWED_ENDPOINTS`(端点白名单,Kilo 不带真实 Token)/ `KILO_CHAT_MIN_INTERVAL`(热更项,独立 pacer、默认 0 = 不节流,与 zen / CB / TRAE 互不排队) |
5555
| Q47 | Qoder(阿里,第五渠道 `qoder`) | 把 [Qoder](https://qoder.com) 接成第五个 provider(`KNOWN_PROVIDERS` 加 `"qoder"`,**无 schema 变更**)。**真实账号渠道**(区别于 zen/kilo 的匿名免费层),走设备码 PKCE 登录,凭证入加密列。协议为私有 COSY:推理 `POST {gateway}/algo/api/v2/service/pro/sse/agent_chat_generation`,body 用**自定义 Base64 变体**编码(三段轮转 + 自定义字母表 + `=`→`$`),头为整套 `cosy-*`,`Authorization: Bearer COSY.<payload_b64>.<md5sig>`,`x-model-key` 路由;签名为 `md5(payload_b64 \n cosy_key \n date \n body \n path)`(`path` 去 `/algo` 前缀,payload 为键排序紧凑 JSON),`cosy_key`/`info` 由临时 AES 密钥经服务端 RSA 公钥加密而来。响应是**信封式 SSE**(`data:{"headers":…,"body":"<内层 chunk>","statusCodeValue":200}`,`body=="[DONE]"` 结束,非 200 判上游错误)。模型发现 `GET {gateway}/algo/api/v2/model/list?Encode=1`(**必须带整套 COSY 签名头**,签名 body 为 `qoder_encode("")`;裸 GET 403、带头 POST/PUT 400,故固定 GET)。额度 `GET {openapi}/api/v2/quota/usage`(`userQuota`+`addOnQuota`),套餐 `/api/v2/user/plan`。签到 `/sash/api/v1/me/daily-check-in/{status,claim}`(409/`ALREADY_CLAIMED` → 已签;**国际版该端点 404 → 视为本区域无此接口,不算错误**)。域:CN `openapi.qoder.com.cn`/`gateway.qoder.com.cn`;Intl `openapi.qoder.sh`/`api1.qoder.sh`。密码学复用项目已有 `cryptography`(不移植参考仓库的手写纯 Python 实现)。新增 `QODER_API_ENDPOINT`(openapi)/`QODER_ALLOWED_ENDPOINTS`(含国内 openapi+gateway 与国际版)/`QODER_CHAT_MIN_INTERVAL`(热更项,独立 pacer、默认 5s) |
56-
| Q48 | CodeArts(华为云码道,第六渠道 `codearts`) | 把 [华为云 CodeArts](https://codearts.huaweicloud.com) 的盘古引擎接成第六个 provider(`KNOWN_PROVIDERS` 加 `"codearts"`,**无 schema 变更**)。**真实账号渠道**,走 OAuth2 PKCE 登录换 STS,凭证入加密列。推理 `POST /api/v2/chat/completions`(福利模型追加头 `maas_type: benefit`);鉴权为华为云 **`SDK-HMAC-SHA256`**(AK/SK + `X-Security-Token`,signedHeaders=请求全部头小写排序,CanonicalURI 每段 encode 且**末尾补 `/`**,payload hash 取 `X-Sdk-Content-Sha256`)。**令牌刷新与 `client_id=codearts-agent` + DPoP 私钥三者绑定、一次性**:`POST {sts}/v1/oauth2/tokens` `grant_type=refresh_token` + **DPoP(ES256/P-256)**,刷后**必须回写新 `refresh_token`**(DPoP 低 S 归一化用 `cryptography` 实现,不移植 Go/手写 ECDSA)。模型:内置 `GET {snap}/v1/model/builtin`(头 `Agent-Type: PromptCenter`)+ 福利 `GET {opengw}/api/v1/gateway/config`;领取 `POST {opengw}/api/v1/benefit/claim`(幂等,启动/定时保活);余额 `GET {opengw}/api/v1/user/tokens/balance`。SSE 为**逐行 `data:` JSON**(无空行),`text` 字段是**累计全文(替换语义,非增量)**,结束 `{"text":"[DONE]","error_code":"0"}`,错误 `error_code` 形如 `ChatAgent.*`。**CodeArts 没有每日签到接口**(免费额度按月重置),故不实现 `checkin`,签到语义由 token 自动 refresh 续期承担。新增 `CODEARTS_API_ENDPOINT` / `CODEARTS_ALLOWED_ENDPOINTS`(snap 引擎 + STS + 福利网关 + 门户)/ `CODEARTS_CHAT_MIN_INTERVAL`(热更项,独立 pacer、默认 5s) |
56+
| Q48 | CodeArts(华为云码道,第六渠道 `codearts`) | 把 [华为云 CodeArts](https://codearts.huaweicloud.com) 的盘古引擎接成第六个 provider(`KNOWN_PROVIDERS` 加 `"codearts"`,**无 schema 变更**)。**真实账号渠道**,走 OAuth2 PKCE 登录换 STS,凭证入加密列。推理 `POST /api/v2/chat/completions`(福利模型追加头 `maas_type: benefit`);鉴权为华为云 **`SDK-HMAC-SHA256`**(AK/SK + `X-Security-Token`,signedHeaders=请求全部头小写排序,CanonicalURI 每段 encode 且**末尾补 `/`**,payload hash 取 `X-Sdk-Content-Sha256`)。**令牌刷新与 `client_id=codearts-agent` + DPoP 私钥三者绑定、一次性**:`POST {sts}/v1/oauth2/tokens` `grant_type=refresh_token` + **DPoP(ES256/P-256)**,刷后**必须回写新 `refresh_token`**(DPoP 低 S 归一化用 `cryptography` 实现,不移植 Go/手写 ECDSA)。模型:内置 `GET {snap}/v1/model/builtin`(头 `Agent-Type: PromptCenter`)+ 福利 `GET {opengw}/api/v1/gateway/config`;领取 `POST {opengw}/api/v1/benefit/claim`(幂等,启动/定时保活);余额 `GET {opengw}/api/v1/user/tokens/balance`。SSE 为**逐行 `data:` JSON**(v2 实测为标准 OpenAI chunk:`choices[].delta` + `data:[DONE]`;旧形状/legacy 才是累计全文 `text`,用快照做差),错误 `error_code` 形如 `ChatAgent.*`。**CodeArts 没有每日签到接口**(免费额度按月重置),故不实现 `checkin`,签到语义由 token 自动 refresh 续期承担。新增 `CODEARTS_API_ENDPOINT` / `CODEARTS_ALLOWED_ENDPOINTS`(snap 引擎 + STS + 福利网关 + 门户)/ `CODEARTS_CHAT_MIN_INTERVAL`(热更项,独立 pacer、默认 5s) |
5757

5858
## 2. 目标与非目标
5959

@@ -130,7 +130,7 @@
130130
### 3.4 CodeArts(华为云码道,第六渠道 `codearts`)
131131

132132
- 域:snap 引擎 `snap-access.cn-north-4.myhuaweicloud.com`;STS `sts.cn-north-4.myhuaweicloud.com`;福利网关 `opengw.developer.huaweicloud.com`;门户 `codearts.huaweicloud.com`
133-
- 聊天:`POST /api/v2/chat/completions`(福利模型追加头 `maas_type: benefit`),SSE 逐行 `data:` JSON,`text` 为**累计全文(替换语义)**
133+
- 聊天:`POST /api/v2/chat/completions`(福利模型追加头 `maas_type: benefit`),SSE 逐行 `data:` JSON,v2 为标准 OpenAI chunk(`choices[].delta` + `data:[DONE]`),旧形状为累计全文(替换语义)
134134
- 登录:OAuth2 PKCE → `POST {snap-manager}/v1/oauth2/tokens`(authorization_code)换 AK/SK/security_token/refresh_token
135135
- 刷新:`POST {sts}/v1/oauth2/tokens`(`grant_type=refresh_token` + **DPoP ES256/P-256**);**refresh_token 与 `client_id=codearts-agent` + DPoP 私钥三者绑定、一次性、刷后必须回写**
136136
- 鉴权:华为云 **`SDK-HMAC-SHA256`**(`Authorization: SDK-HMAC-SHA256 Access=<AK>, SignedHeaders=…, Signature=…`)

‎TECHNICAL.md‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,8 @@ coding2api/
8888
│ │ ├── codearts/
8989
│ │ │ ├── signer.py # 华为云 SDK-HMAC-SHA256(纯函数,便于固定向量测试)
9090
│ │ │ ├── dpop.py # ES256/P-256 DPoP JWS(cryptography,低 S 归一化)
91-
│ │ │ ├── client.py # CodeArts 上游 + 累计全文 SSE 还原 + 福利/余额 + Provider
92-
│ │ │ ├── events.py # 累计全文 SSE → 增量 Event;状态/业务码分类
91+
│ │ │ ├── client.py # CodeArts 上游 + 私有 SSE 还原(OpenAI chunk/累计全文双形状)+ 福利/余额 + Provider
92+
│ │ │ ├── events.py # SSE → 增量 Event(支持 OpenAI chunk 与累计全文两形状);状态/业务码分类
9393
│ │ │ ├── credential.py # 凭证类型与解析(AK/SK/STS/DPoP/refresh)
9494
│ │ │ ├── auth.py # OAuth2 PKCE 登录(authorize URL + 换 token)
9595
│ │ │ └── oauth.py # 登录适配:回调链接粘贴换 token(AuthStateStore/start/complete_callback)
@@ -644,13 +644,13 @@ UA 版本走 `ZEN_OPENCODE_VERSION` 配置(上游改阈值改 env,不硬编
644644

645645
### 3.17 CodeArts 渠道(Q48)
646646

647-
**与其余五条都不同**:CodeArts 既不是标准 OpenAI、也不是 COSY 那种「自定义 body + 信封 SSE」,而是**华为云签名体系 + 累计全文 SSE**。
647+
**与其余五条都不同**:CodeArts 既不是标准 OpenAI、也不是 COSY 那种「自定义 body + 信封 SSE」,而是**华为云签名体系 + 私有 SSE 帧**。
648648

649649
**SDK-HMAC-SHA256 签名**:鉴权用 AK/SK + `X-Security-Token`。`Authorization: SDK-HMAC-SHA256 Access=<AK>, SignedHeaders=…, Signature=…`。三处与常见 HMAC 不同、必须逐字节对齐:signedHeaders 是**请求全部头**(小写、字典序);CanonicalURI **每个路径段单独 percent-encode 且末尾补 `/`**;payload hash 取 `X-Sdk-Content-Sha256`。`signer.py` 写成纯函数以便固定向量测试。参考实现是 Go(`signer.go`),本项目用标准库 `hmac`/`hashlib` 重写。
650650

651651
**DPoP 令牌刷新(刚性约束)**:`POST {sts}/v1/oauth2/tokens` `grant_type=refresh_token` 需带 **DPoP(ES256/P-256) JWS**。refresh_token 与 `client_id=codearts-agent` + DPoP 私钥**三者绑定、一次性**——刷新成功必须把返回的新 `refresh_token` 回写凭证(`RefreshTask` 先落库再同步账号,正好满足)。ES256 用 `cryptography`(`ec.SECP256R1` + `ECDSA(SHA256)`,**低 S 归一化**),不移植 Go/手写 ECDSA。
652652

653-
**累计全文 SSE**:上游逐行 `data:` JSON(**没有空行分隔**,部分行无 `event:` 前缀),`text` 字段是**累计全文(替换语义,非增量)**——每帧都带从头到尾的完整文本。解析层按「新帧 text 以旧帧为前缀」时的差量产出 CONTENT 事件,对客户端透明为增量流;结束帧 `{"text":"[DONE]","error_code":"0"}`,错误 `error_code` 形如 `ChatAgent.*`。
653+
**SSE 帧(2026-09-30 抓真实流核实)**:逐行 `data:` JSON(`data:` 行间有空行;也有不带 `data:` 前缀的裸 JSON 行),最后由 `data:[DONE]` 结束。**v2 `/api/v2/chat/completions` 实测是标准 OpenAI chunk**:`{"choices":[{"delta":{"content":…,"reasoning_content":…,"tool_calls":…},"finish_reason":…}]}`,增量在 `delta`(**不是**累计全文),收尾帧 `choices:[]` + `usage` 单独给 token 数;带 `tool_stream:true` 时工具调用分片在 `delta.tool_calls`。旧形状(逆向记录 §5 / legacy `/v1/chat/chat`)则是 `{"text":"<累计全文>"}`(替换语义,用 `TextSnapshot` 做差)+ 结束帧 `{"text":"[DONE]","error_code":"0"}`。解析器**两种形状同时兼容**,按字段分派。错误有两条路:HTTP 非 2xx,或流内 `error_code`(形如 `ChatAgent.*` / `TM.00001041`,HTTP 仍 200)。
654654

655655
**无每日签到**:免费额度**按月重置**,上游没有每日签到接口。因此本渠道**不实现 `checkin`**(`checkin_scope` 也一并省略,后台签到任务自动跳过它);「保活」由 token 自动 refresh 承担。福利模型发现(`{opengw}/api/v1/gateway/config`)与 Token 领取(`POST /api/v1/benefit/claim`,幂等)在探测时顺带完成。
656656

0 commit comments

Comments
 (0)