mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-07 09:47:23 +00:00
fix(codearts): 上游限流改为可声明重试(内核通用能力)+ 会话/连接两处真因
用户连续报「codeart 还是不可用 / empty stream」。把上游原始响应打出来逐条对齐,
三个症状同源,且都不是"模型坏了":
| 场景 | 上游实际返回 |
|---|---|
| 流式、短时间连续请求 | HTTP **200** + 只带 error 的帧 ⇒ 内核判 `empty stream` |
| 非流式、同一条件 | HTTP 400 `TM.00001041: 并发会话数已达上限(3个)` |
| 突发请求 | HTTP 429 `TPM limit … 500000 tokens per minute` |
实测:最后一个请求结束后约 **44~60 秒**释放;同一账号连续请求 3~4 次即占满。
参考实现(示例插件 llm-adapter.ts)的做法是**每 10s 轮询后重试 chat**(最多 30
分钟),所以用户那边"看起来没事"。
## 我们的改法(内核只做通用机制,厂商知识留在插件)
1. **内核新增声明式重试策略** `source.retry`(纯数据:状态码列表 + 响应体子串 +
间隔 + 次数)。内核不认识任何厂商;命中即在同一网关请求内每 10s 重试一次。
流式只在**握手阶段**重试(首帧尚未下发,不会重复输出),非流式整体重试。
2. **插件为 codearts 声明**:400/429 + 命中 `TM.00001041` / `TPM limit` /
`并发会话数已达上限` → 10s × 最多 18 次(≈3 分钟)。
3. **适配器补两个会话细节**:`Connection: close`(keep-alive 空闲连接会被上游
计入并发会话)与稳定 `Session-Id`(同一账号恒定,避免每次请求新建会话)。
4. 错误文案补可行动提示(并发会话上限 / TPM 限流,约 1 分钟释放)。
## 验证(生产)
- 连续 6 次**流式**:6/6 成功(撞上限时自动等 10s 再试,客户端只感觉慢):
`+0s / +12s / +34s / +86s / +100s / +144s`
- 连续 5 次**非流式**:5/5 成功(`+0 / +11 / +20 / +28 / +38s`)
- 修复前:第 4 个请求起必然 TM.00001041 / empty stream。
> 注:`TM.00001005 无正常可用套餐` 属于账号权益问题(不重试、如实报出),
> 与限流要分开处理。
(cherry picked from commit 6454b094bc)
This commit is contained in:
@ -1046,6 +1046,24 @@ local PLATFORMS = {
|
||||
base_url = "https://snap-access.cn-north-4.myhuaweicloud.com",
|
||||
-- 本插件**自己装的**适配器(见 installCodeartsAdapter):华为云 AK/SK 签名
|
||||
adapter = "codearts",
|
||||
-- ★ 上游的"不可用"多数是**账号级限流**,约 1 分钟自愈,不是模型坏了:
|
||||
-- * 400 `TM.00001041: 并发会话数已达上限(3个)` —— 账户并发会话上限,
|
||||
-- 实测最后一个请求结束后约 44~60 秒释放;
|
||||
-- * 429 `TPM limit ... 500000 tokens per minute` —— 每分钟 token 配额。
|
||||
-- 内核原本**只在 429 上做冷却、不重试**,于是这类"等一会儿就好"的拒绝
|
||||
-- 直接冒到客户端(流式还表现为 `empty stream`,因为上游用 HTTP 200 +
|
||||
-- error 帧表达拒绝)。
|
||||
--
|
||||
-- 这里声明**声明式重试策略**(内核只按数据匹配,不认识 CodeArts):
|
||||
-- 命中即在网关内部每 10s 重试一次,最多 18 次(≈3 分钟),客户端只是
|
||||
-- 多等几秒,不会看到错误。流式只在**握手阶段**重试(首帧尚未下发),
|
||||
-- 不会重复输出。
|
||||
retry_policy = {
|
||||
on_status = { 400, 429 },
|
||||
on_body_contains = { "TM.00001041", "TPM limit", "并发会话数已达上限" },
|
||||
delay = "10s",
|
||||
max_attempts = 18,
|
||||
},
|
||||
-- 签名平台:源注册要交**完整凭据 JSON**(适配器自己解 AK/SK 签名),
|
||||
-- 不是裸 access token。见 syncAccountSource 的注释。
|
||||
credential_shape = "full",
|
||||
@ -4588,6 +4606,9 @@ local function syncProviderSource(provider)
|
||||
if plat.endpoint then src.endpoint = plat.endpoint end
|
||||
if plat.endpoint_stream then src.endpoint_stream = plat.endpoint_stream end
|
||||
if plat.headers_extra then src.headers = plat.headers_extra end
|
||||
-- 声明式上游重试策略(见平台上的 retry_policy 注释):内核只按 status +
|
||||
-- body 子串匹配,不认识任何厂商。
|
||||
if type(plat.retry_policy) == "table" then src.retry = plat.retry_policy end
|
||||
-- ★ 把**设备身份**随源带给适配器:有些平台的头不是简单 Bearer,
|
||||
-- 需要 uid / device_id / machine_id 派生(trae 的 X-Uid / X-Device-Id /
|
||||
-- X-Machine-Id)。适配器沙箱里**读不到凭据**,只能靠 meta 传。
|
||||
@ -10037,7 +10058,21 @@ end
|
||||
-- build_headers:内核按源注入 meta{url,method,body,api_key,...}。
|
||||
-- 凭据是完整 JSON(登录时存的就是它),从中解出 AK/SK/ST。
|
||||
function adapter.build_headers(meta)
|
||||
local h = { ["Content-Type"] = "application/json" }
|
||||
-- ★★ Connection: close 是**必须**的,不是优化。
|
||||
--
|
||||
-- CodeArts 后端按**账户**限制并发会话数(3 个),而它把"会话"绑定在
|
||||
-- HTTP 连接上:内核的 Go transport 会保留空闲连接(默认每 host 2 条)
|
||||
-- 用于 keep-alive,于是第 3~4 个请求就被判成"并发会话数已达上限(3个)"
|
||||
-- (TM.00001041),用户看到的是 codeart 连续不可用。
|
||||
--
|
||||
-- 实证(2026-10-20):
|
||||
-- * 走网关连续 5 次 → #1/#2/#3 成功,#4/#5 全部 TM.00001041;
|
||||
-- * 同一凭据、每请求**新连接 + Connection: close** 连续 5 次 → 5/5 成功;
|
||||
-- * 之后连续流式也一样:3 次成功后开始 400。
|
||||
-- 即"会话"就是连接,关掉连接会话立刻释放。
|
||||
--
|
||||
-- 代价是每次多一次 TCP/TLS 握手(~百毫秒级),换"渠道真的能用",值得。
|
||||
local h = { ["Content-Type"] = "application/json", ["Connection"] = "close" }
|
||||
local raw = tostring(meta.api_key or "")
|
||||
local cred = nil
|
||||
if raw:sub(1, 1) == "{" then
|
||||
@ -10055,6 +10090,19 @@ function adapter.build_headers(meta)
|
||||
return h
|
||||
end
|
||||
for k, v in pairs(signed) do h[k] = v end
|
||||
-- ★★ Session-Id 是**必须**的,不是可选项 —— 它决定"并发会话数"怎么算。
|
||||
--
|
||||
-- 实测(2026-10-20,对照参考实现 llm-adapter.ts:1168 `headers.set('Session-Id', …)`):
|
||||
-- 不发送会话号时,后端把**每个请求**当成一个新会话;而会话在响应结束后还要
|
||||
-- 存活一段时间才释放,于是连续 3 个请求就把账户的「并发会话数上限(3个)」占满,
|
||||
-- 之后所有请求(含流式)稳定回 TM.00001041 / 200+空流(用户看到 empty stream)。
|
||||
-- 发一个**稳定**的会话号(同一账号内恒定),后端复用同一会话 ⇒ 不再累加。
|
||||
--
|
||||
-- 会话号取 AK 的 sha256 前 32 hex:稳定、可复现、不引入需要持久化的状态。
|
||||
local ak = tostring(cred.access_key_id or cred.access_key or "")
|
||||
if ak ~= "" then
|
||||
h["Session-Id"] = string.sub(sha256_hex(ak), 1, 32)
|
||||
end
|
||||
return h
|
||||
end
|
||||
|
||||
|
||||
Reference in New Issue
Block a user