Repository navigation
Releases: shuishuipingan/qoder2api-hub
Release list
v1.3.7
✨ 新增功能
① 双区自动路由的完整体验(#26 续) —— v1.3.6 把自动路由发出去了,这一版把它变成看得见的:
-
「本次实际出口」不再靠猜:usage 行新增
realm_reason(三态),面板据此给出确定结论:realm_reason含义 面板显示 model_missing当前区官方清单里没有这个模型 「模型不在默认出口,已自动改走 X 区」 no_account模型本区有、被账号可用性推走 「默认出口无可用账号,已自动改走 X 区」 ""(显式空串)明确没有换区 「按默认出口执行」 键不存在 字段上线前的旧行 「未知(旧记录)」—— 不给用户一个假结论 -
最近请求表新增「区域」列:直接来自 usage 行的
realm—— 反映的是每次对话的真实出口,不是面板自身请求; -
带时间:「最近一次对话 12:03 走了国内版 · 未换区」—— 持久化字段才有的信息量。
🐛 问题修复
- 面板数据源从响应头迁到 usage 行:v1.3.6 冻结的契约(
X-Qoder-Realm只在 chat 类请求出现)让面板拿不到那个头(面板发的都是管理端点)→ 改为读 usage 行,绕开长连接状态泄漏那一整类问题,而且历史可查; - 反向保险:新增「
dashboard.html全文不得再读X-Qoder-Realm」的断言(检查目标是被守护的文件,不是测试自己),并用金丝雀自证(注入一行读取 → 必红)证明它不是摆设 —— 两次独立跑过; - 两条「片段依赖自检」:测试用的 eval 片段必须自带
RECENT_SNAPSHOT声明,否则当场红并说明原因(而不是崩溃成一个读起来像产品坏了的 ReferenceError);并断言该声明在dashboard.html里顶层唯一、引用都在函数体内 —— 把「浏览器里会不会炸」变成可执行断言。
🎨 体验优化:见 ① 的三条。
- 口径收紧两处:最近一次对话取「最近一条非 error 行」(失败行可能没有真实出账区,拿它当结论是错的);旧行显示「未知(旧记录)」而不是「没换区」;
- 流程:报告「全绿」时附两枚指纹(被测文件 md5 + 套件副本 md5)—— 本轮一次误报(过期副本被读成产品故障)就是靠这条把范围锁死的;复核方也贴自己跑的那份,先对齐再谈结论。
验证
- 双入口全绿:
python tests/run_all.py→ 28 suites / 1005 checks / 0 failed(前端六套件 105 = 9/19/19/15/12/31);python _test_qoder.py→ 626 passed / 0 failed; - 金丝雀两次独立跑过(作者一次、复核者一次):注入读取 → 反向保险必红;真文件 → 全绿。
v1.3.6
✨ 新增功能
① 双区自动路由(#26) —— 不用再手动切面板:
- 模型独占时自动试另一区:某模型只在一个区有的(7 个 ——
gm51model/q37fmodel仅国内,cmodel/efficient/performance/smodel/ultimate仅国际)会自动改用那个区; - 无可用账号时回退另一区;
- 面板三态开关:自动 / 锁定国内 / 锁定国际(「自动」由
QD_AUTO_REALM控制、改它需重启;锁定走既有的区域切换); - 实际出口与原因可见:换区时响应带
X-Qoder-Realm与原因X-Qoder-Realm-Reason: model_missing | no_account;面板据此显示「最近一次请求的实际出口与原因码」。⚠️ 口径说明:面板那处显示反映的是面板自身请求(/usage、/v1/models、/realm)有没有被自动换区,不是「每个客户端对话走了哪个区」——后者要看 usage 行的区域维度(「最近请求表补区域列」已排入下一版);
/v1/models?merge_realms=1:两区并集视图(含区域标注与realm_exclusive),opt-in —— 默认仍是单区视图。
边界与原则(重要)
- 显式优先:
?realm=/ Key 绑定 /X-Realm永远优先于自动路由,自动只在用户没指定时兜底; - 判得出才动:只在「模型独占」或「当前区无可用账号」时换区,判不出就维持现状;
- MiniMax 那两个是数据驱动排除(同 key 两区型号不同即不猜),不是硬编码黑名单 —— 未来同类模型自动覆盖;
- 跨区会断会话亲和 —— 这是接受的:那两种情形下当前区本来就没有可用的亲和目标,解绑的是一条已失效绑定;
- 不违反限额护栏:护栏是账号级的,换区即换到另一个账号,它自己的阈值照常生效。
🐛 问题修复
- 路由规则叠加时的优先级错误(本轮自查发现):账号回退逻辑只看账号数,会把「模型规则已经推走的区」按账号换回原区 —— 全局在 cn、请求仅 intl 有的模型、而 intl 恰好没号时,请求会被送回 cn,打到「没有这个模型」的上游,用户看到的是语义错误而不是「没号」。已修:模型归属优先于账号可用性。
🎨 体验优化
- 看板:自动换区与原因可见(三态开关 + 「本次实际出口」),并严守一条原则:只在真的抓到响应头时才说「自动换区」;没抓到显示「未观察到换区」;有头但与默认出口一致时显示「(与默认出口一致)」—— 不给假指示。
tests/_tools/进仓:dash_extract.js(锚点缺失即抛 + 按函数做纯度检查)、dep_scan.py(迁移依赖扫描)+ 两个自测 —— 后来人可直接复用这些判断;tests/README.md新增「基线数字(提交前核对用)」:28 suites / 989 checks / 前端 89 / legacy 630,附提交前三步 —— 其中一条是「用git add -A,不要只用具体文件名」(新增套件会被静默漏掉,表现是全绿但总数变小)。
验证
- 双入口全绿:
python tests/run_all.py→ 28 suites / 989 checks / 0 failed;python _test_qoder.py→ 630 checks / 626 passed / 0 failed; - 自动路由逐条实测:7 个独占模型 7/7 正确、双区都有的 12 个 12/12 保持当前区、显式优先级六条全对、换区可见(日志 + 响应头)、并集 19 条去重正确;
- 能红证据:改动前那 7 个模型在当前区不可用(输出已留存)。
v1.3.5
✨ 新增功能
- Linux / macOS 启动脚本(社区贡献 @jk-uzi):
start-qoder-proxy.sh/start-qoder-proxy-lan.sh/allow-firewall.sh,与 Windows 的.bat一一对应;运行时探测链相同、要求 Python 3.9+、用exec让终端生命周期等于进程生命周期;allow-firewall.sh检测 ufw/firewalld/nftables/iptables、用-C保持幂等并打印回滚命令。
🐛 问题修复
- Anthropic 入口的 thinking 档位被静默丢弃(@jk-uzi):
/v1/messages收下thinking.budget_tokens却没有把档位表传下去 → 每个请求都静默跑默认档位。这是我们在接线时漏掉的一步(桥接设计文档里写过)。已补catalog_meta_for()并把 meta 传进转换层。 - 非对象上游帧会让流崩溃(@jk-uzi):信封
body可能是数组或标量,三处直接.get()就抛AttributeError→ 流式中途崩、聚合整单失败。现在按类型分流(放行 / 丢弃 / 不动 inner)。 - 双区账号池下冷却判断用错口径(#29 @ismodo):冷却函数问「全局有几个账号」,选号循环问「本区域有几个可用」——双区池里两数不等,本区域唯一的账号被当成多号池,于是上游请求的 30/60s 冷却原样生效、超出等待预算,一次本可 3 秒自愈的排队变成中断。两处现在共用同一个 helper;等待逻辑改为收集候选取最小值(不再因某个号的长频控而放弃其它号的短冷却)。
- Windows 下网关会因系统代理开关而全挂(#27 @mengmengma0104):
urllib在 Windows 会回退读 WinINET 注册表,且默认 opener 在首次请求时固化代理配置 → 代理工具关闭后网关仍去连已无人监听的端口,直到重启。现在自建 opener、只从环境变量取代理:设了HTTPS_PROXY/HTTP_PROXY/ALL_PROXY就走它,没设就直连(不回退注册表),且每请求现读、无固化。 - intl 账号账本被写全 0 / 活动文案误报 /
lastCheckin恒空(#25 @xiarunjie):- 账本写入此前无任何合理性守卫 —— 一个退化响应(exceeded + 两个 quota 全 0 + CN 的永不过期占位)会把健康的 399 覆盖成 0,而且不记录原始响应,事后无法回溯。现在:保守判定(真·余额归零照常覆盖)、拒绝覆盖时落盘截断的原始摘要、调度器排除本轮凭证失效的账号(不再用失效 token 打上游);
- 看板与一键签到把已领取的奖励报成「暂无可领取」—— 因为过滤发生在归类之前,而 intl 那条活动的
actionType不是CLAIM_BENEFIT。现在先归类再决定是否参与领取;lastCheckin也改成已领取时同样盖章(此前只在「我们自己去领」时盖章 →can_checkin恒真 → 调度器每轮重走同一个号)。
- 批量刷新路由对账号集合没有守卫:
/accounts/credits(GET/POST)、/accounts/refresh会对禁用号与坏凭证号也发请求 → 它们的账本被反复零化。现在统一走_refreshable_accounts(),被跳过的号以{uid, reason}出现在响应的skipped里(不静默少项)。
🎨 体验优化
- 看板区分「未知」与「0」:此前
energy || 0让None与0塌成同一个显示 —— 这正是 #25 报告者被误导的界面症结。现在三态分明(正常数字 / 未知(warn 色 + 原因)/ 真 0);批量视图的合计会标注「已排除 N 个未知账号」并列出是哪些。
⚠️ 一条语义变化(#29 的连带):频控窗口 ≤10s 时,网关会等待并重试,客户端收到的是延迟后的成功而不是 429。这是自愈(比中断好),但与旧行为不同;>10s 的长频控不受影响,仍返回 429。- 测试:编排总数到 974 条;
tests/README.md增补「桩的保真度」纪律(元素桩照 DOM 语义做、兄弟函数显式注入空桩、锚点缺失即抛、按函数而非区间做纯度检查)。
验证
- 双入口全绿:
python tests/run_all.py→ 27 suites / 974 checks / 0 failed;python _test_qoder.py→ 630 checks / 626 passed / 0 failed; - 每条修复都带能红证据:冷却四组数字(30→3 / wait 0→3 / 长窗保持 0 / 多号坑 0→3)· 账本污染前后(
ZEROED: ['u-disabled','u-no-cred']→[],且断言calls == 0)· 文案矩阵三档一致 · CN 对照组逐字 SAME(8/8) · 非对象帧的 8 条 FAIL 自证; - 一条流程留痕:
VIEW_DETAILS该不该「算已领」出现过新旧断言冲突,最终判定不该把显示与盖章分开(分开会重现lastCheckin恒空那条链),并把原断言拆成 E3a/E3b 各自钉住新语义与「防过度」的界。
v1.3.4
⚠️ 如果你在用1.3.2或1.3.3的预构建镜像,请升级到本版 —— 那两个镜像里少了一个模块文件,容器会启动即崩(ModuleNotFoundError: No module named 'qoder_anthropic')。本版已修复,并加了 CI 冒烟防止再犯。
🐛 问题修复
- 镜像漏 COPY
qoder_anthropic.py(issue #23,报告者 loki0411):qoder_proxy.py从 1.3.2 起import qoder_anthropic,但Dockerfile的 COPY 清单是逐个文件枚举的,新增模块时漏了它 → 镜像里没有这个文件 → 容器启动即ModuleNotFoundError、无限重启。- 报告者的排查很完整(镜像内
find无该文件、镜像内qoder_proxy.py的 sha256 与 tag 一致、对照 1.3.1/1.2.19 自洽可启动),我们照此复现并修复; - 为什么我们的测试没抓到:所有测试都跑在源码树上,而这是镜像内容问题;CI 的构建步骤不会因为漏 COPY 而失败(构建本身成功)。四个发布的验证里,我们一次都没真正运行过容器 —— 这是流程缺口,不是测试缺口;
- 受影响范围:
1.3.2与1.3.3的预构建镜像(latest在 v1.3.4 发布前指向 1.3.3,因此也受影响)。源码运行不受影响; - 临时绕过(报告者提供,仍可用):compose 里挂只读文件
- ./qoder_anthropic.py:/app/qoder_anthropic.py:ro;升级到本版后可移除。
- 报告者的排查很完整(镜像内
✨ 新增功能
- CI 冒烟(防复发):构建之后新增一步,用本地单架构镜像实际跑三件事 ——① 按源码里的
import qoder_*反查每个模块文件是否真的在镜像里(这次就是这么漏的,将来任何新增文件忘 COPY 都会立刻红);②import qoder_proxy, qoder_anthropic确实可导入;③ 起容器并等/ping回应(60 秒超时,失败打印容器日志)。一句话教训:构建成功不等于镜像能跑。
- README 安装段补了镜像与版本直达入口(GHCR 包页面 + Releases 链接、可用 tag 形状、双架构说明、以及「仓库侧栏未必列出容器包」的提示)。
验证
- 双入口测试全绿:
tests/run_all.py→ 19 suites / 881 checks / 0 failed;_test_qoder.py→ 630 checks / 0 failed; - 镜像层验证由新增的 CI 冒烟承担:本版 CI 里会执行上面三步(其中第 ① 步在修复前必然失败 —— 因为
qoder_anthropic.py不在镜像里)。
v1.3.3
✨ 新增功能
① 前端测试破题(工作包点名的第五类空白):tests/ 新增 4 个前端断言套件(62 条),把前面几轮的前端证据从会话里搬进仓库:
effort_parity(9 条) —— 逐行审计列与汇总视图的四类计数一致性。这是这批里最有价值的一条:同一份数据两处实现,口径一旦漂移,运维就会看到互相矛盾的数(与我们sum(by_key)==sum(by_account)是同一类问题);limits_auth(19 条) —— 403/401 分流(把 403 当鉴权失效会让用户白白掉登录、真原因被吞 —— 这是修过的真 bug 的回归网)+ 限额保存载荷形状(空串=继承 / 0=关闭 原样传递);effort(19 条) —— 档位三态渲染 + Key 列四态;log_and_badge(15 条) —— 日志增量追加的行为断言(追加而非重建)+ 护栏四态徽标。- 做法:自读
dashboard.html+ 抽 script 块 + 内联元素桩,没有引入 jsdom(守住零依赖红线);配两个工具:tests/_tools/dash_extract.js(锚点缺失即抛 —— 防「切片拿到空串、断言照样通过」这种假绿)与dep_scan.py(迁移依赖扫描)。
② 按 Key 的档位分布(矩阵下方):一眼看出哪个 Key 受影响最多(多少请求被挪档、多少被丢弃)。这是把 P1-3 的审计口径从「逐行可见」升级成「按 Key 汇总可见」;零额外请求(复用已拉回的最近请求行);口径行明写「基于最近请求页样本,不是全量聚合、数字不可相减」。
🐛 问题修复:Key 列 tooltip 不再出现后端桶名的字面量 —— 界面(含 tooltip)均不泄露实现细节。
🎨 体验优化:见 ② 的汇总视图。
- 测试迁移策略调整(有数据支撑,不是妥协):写了一个依赖扫描工具(提取每段定义的全局名 → grep 后续段引用),跑完 55 个段后
--leaf-only没有任何输出 —— 不存在纯叶子段(段间共享显式全局如time/base64,也有隐式的、供后续段继承的绑定)。
结论:逐段迁移在这个文件结构下,边际成本已高于收益。 因此调整为:legacy 作终态(它是在用的测试文件、630 条断言、持续全绿,不是技术债)、新测试一律走新套件、迁移只做低风险部分 —— 让它自然冻结,而不是强行拆完。 - 迁移期三条纪律(三次回滚换来的):① 迁移与修 bug 不混在同一次编辑;② 共享头部显式抽出(别让每个套件自己抄 import);③ 先做依赖扫描(只有叶子段适合独立成套件)。
验证
- 双入口全绿:
python tests/run_all.py→ 19 suites / 881 checks / 0 failed;python _test_qoder.py→ 630 checks / 626 passed / 0 failed / 4 skipped; - 4 条「改坏 → 必红」自证:parity(9 FAIL)· auth(7 FAIL)· log(10 FAIL)· effort(EXIT=1)—— 每条新套件都证明了自己真的在守,而不是「看起来绿」;
- 一条工具经验:改坏点里含非 ASCII 时 PowerShell 的转义会坑人(试两次没命中),改用 Python 按行号替换一次成功 —— 已记进团队纪律。
v1.3.2
✨ 新增功能
① Anthropic 原生入口 /v1/messages —— Claude Code 这类 Anthropic 客户端可直接接入:
- 新增独立转换模块
qoder_anthropic.py(零依赖纯标准库):入站映射(14 个字段)、出站(非流式 + 流式事件流)、usage 守恒映射、effort 断点映射(逐模型 clamp + 单调不降); - 接线的三条要害(都配了断言):①
x-api-key头(Anthropic 客户端不用 Bearer,漏了会全量 401);② 形态决策单点(结构化与否只判一次;两处各判会出现「桥接层以为结构化、flatten 走文本化」的错配);③ 守卫链逐字复用:sse_with_heartbeat(recover_leaked_tool_calls(iter_inner_sse(...), allowed_names=...)),且allowed_names必须取转换后的 chat 请求(用 Anthropic 原始 payload 会取不到工具名 → 回读守卫失效); - 泄漏回归(新入口):#8 / #9 / #11 三条历史样本走新入口 → 守卫仍然生效(有日志实证),#8 还能正确还原成
tool_use块。新入口没有变成第三个绕过守卫的门; - 三条已知差异(已写进 README 已知限制):流式不做流内信封重开(
message_start已发出、重开等于两个 start,协议违规;改为发明确的event: error终态事件而非静默关流;非流式仍完整重试)· 默认不发 thinking block(Anthropic 要求signature,我们生成不了也不伪造;QD_ANTHROPIC_THINKING=empty为实验开关)· 入站 thinking 一律丢弃。
② 模型闸门:全局 banned_models + 每个 API Key 的 models 白名单(fnmatch 通配)。
- 统一落点
_model_precheck、三个入口共用(chat / responses / anthropic)—— 不是三份实现,天然没有「某个入口绕过」的缺口(已用断言覆盖/v1/messages); - 拒绝发生在本地:出站层与网络层两层计数均为 0;并含对照组(白名单内放行)与默认不变(空配置放行)—— 防「一律拒绝也绿」;
- 同理,
_clean_key_entry是白名单重建,models必须显式保留(否则每次保存被静默丢弃 —— 与早前 limits 同型的坑,已断言钉住)。
③ 会话亲和长度上限:QD_AFFINITY_MAX_MSGS(默认 0 = 关闭)。
🐛 问题修复:无
🎨 体验优化:Anthropic 入口的三条已知差异写进 README(读者需要知道边界在哪)。
tool_choice="none"形态对齐:从「连tools一起删」改为「保留tools+ 写回 none」。上游 body 对比确认只影响tools一个字段(其余 7 个非随机顶层键全同);开关QD_TOOLCHOICE_NONE_DROPS_TOOLS=1可一键回退。⚠️ 一条反例(如实登记):参考项目自己的注释说「其上游并不真正遵守 none」(保留 tools 后仍回tool_calls),与我们工作包对 qoder 的实测相反。我们按 qoder 的实测默认新行为,但留了回退开关 —— 若观察到none下仍回tool_calls,设它即回旧行为。
- 出站收口(P1-2 第一步):把带凭证的直连收进统一出站函数(不变量基线 3 → 2,且中间态红过,证明断言在守);并判定「按账号出站代理」不做 —— 账号全部由用户本机导入(出口天然相同)、全局代理已零代码支持(
HTTPS_PROXY,README 已补)、无任何「上游按 IP 风控」信号。若将来真撞上,最小方案约 25 行(已留档)。 - 测试工程化:
tests/编排现 15 个套件 / 819 断言;从旧单体文件已迁出 4 段(每批双入口对比、账目精确);迁移期两条纪律已写进tests/README.md(路径一律基于__file__;每套件自己的临时目录、不写仓库内共享路径)。
验证
- 双入口全绿:
python tests/run_all.py→ 15 suites / 819 checks / 0 failed;python _test_qoder.py→ 626 checks / 0 failed / 4 skipped; - 本轮新增能红证据 3 组:Anthropic 模块(改前
ModuleNotFoundError)· 接线(4 红 → 16 绿)· 模型闸门(4 红 → 16 绿,明细显示未实现时请求放行到出站口); - 旁路核查:确认模型闸门三入口共用同一判定,不存在「chat 被拦、messages 绕过」;
- 一处自我更正也已登记:某次
run_all出现过 1 failed,追查为撞上并发编辑的中间态(不是回归);重跑 0 failed。这也提醒:并行开发期的瞬时红要先排除中间态再当回归。
v1.3.1
✨ 新增功能
① 测试工程化 —— tests/ 并发编排上线:
tests/run_all.py:并发、每套件独立日志、失败打 tail 25 行、强制 UTF-8、无匹配套件返回 2(防「0 passed / 0 failed → exit 0」这种假绿);- 11 个套件 / 747 条断言,含工作包点名的三类空白:代理不变量(参数穿透式断言 + 基线登记)、keep-alive 早拒(裸 socket)、前端 DOM 断言(首个
.js套件,自读 dashboard.html + 内联元素桩); - 兼容入口保留:
python _test_qoder.py照旧可用;套件正逐段从它迁出(本轮已迁 2 段,legacy 仍承载其余 —— 迁移期间总数只增不减); ⚠️ 一处更正:工作包让照抄的 wbtests/_dom_stub.js并不存在(递归查 0 命中);它的真实做法就是「每个套件自读 dashboard.html + 内联手写约 15 行元素桩」,我们按后者实现,没有引入 jsdom(保住零依赖)。
② 账号退避 / 熔断 / 降权三族:
note_soft_rate(软限流指数退避:600 → 1200 → 2400,封顶 7200)·note_failure(硬错误熔断)·note_unknown_failure(未知错误降权)·note_success(统一复位入口);- 接线 6 处 —— 这正是教训所在:三族在接上调用点之前是死代码(有断言证明接线前 6 处计数全 0);
- 防误接(反例断言):直连 401/403 · 信封 429(10605 排队,接了会让一次排队冷掉全账号) · 内容审核 · 402 余额不足 · 单号池 → 三族零调用;
- 单号池不接(
total>1条件):单号池接熔断/长退避 = 变相打死账号; - 运维恢复:三族状态是内存态(不落盘)→ 重启即清 / 等窗口自动过(30 分钟起、封顶 6 小时);面板的「重新启用」走
clear_error,有意不清熔断。
③ 用量字段与聚合:
- usage 行补
reasoning_effort/reasoning_effort_requested/key_id(全脱敏);三态可辨:同值 = 原样透传 · 不同 = 被挪档 · 空串 = 请求了但未下发; - 新增
GET /usage/by-key(与/usage/by-account逐字对称;sum(by_key) == sum(by_account)不变量已断言;缺key_id的行归(no-key)桶、不丢弃); - 面板:最近请求表加 Key 与 档位 两列(档位三态一眼可辨)· 指标页新增按 API Key 用量透视(Key / 请求数 / 输入 / 输出(含思考)/ 缓存 / 总 token / 占比 / 积分 / 模型前 5,按 token 降序)。
🐛 问题修复
- 临期积分分派:管道就绪、当前不触发 —— 实测上游
/quota/usage不返回 per-package 到期字段(只有顶层expiresAt,且它可能是「按月重置」而非「到期作废」)。处置:防御性透传(上游一旦补字段即自动生效、无需改码)+ 刻意不把顶层expiresAt贴到包级 —— 硬贴会让「按月重置」的额度被优先消耗(反向浪费)。
🎨 体验优化:见 ③ 的两处 UI(档位三态 + 按 Key 透视)。
- 测试方法论(写入团队配方):parity 类断言只能用
requests/ token /credit(models是前 5 名截断的展示字段,拿它求和必假失败);error 行两侧都跳过;窗口按row["at"](epoch)过滤、完全不看iso,且缺at的行被当 0(夹具必须给at);窗口边界是闭区间;桶数不等是正常语义(一个 Key 可打多个账号)。 - 验证配方补充:任何「逐字节一致」类对照必须用固定夹具或同一份快照 —— 真实日志仍在被追加(
analytics统计 error 行、by_account跳过),否则会把「日志在长」误判成「改动引入差异」。
验证
- 双入口全绿:
python tests/run_all.py→ 11 suites / 747 checks / 0 failed;python _test_qoder.py→ 658 checks / 0 failed / 4 skipped; - 每个新套件都有「改坏 → 必红」自证(本轮 6 条:护栏等号边界 · 代理绕过点 · 前端配色 · 三族接线计数 · parity 口径 · dashboard 渲染);
- 三族接线:接线前 6 红(计数全 0,正是 6 个待接点)→ 接线后 13 绿;
- parity 修绿的过程本身也是个证据:期望值先按实测口径重算(无窗口 645 / day 145),而不是改实现去迁就断言。
v1.3.0
自 v1.2.12 以来的主要变更概览:机器身份落盘缓存 + 种子机制修正(容器重建不再换设备)、VM 抖动多数表决(不把身份固定在少数派)、工具历史结构化直传、只读积分路由
GET /credits/summary、usage credit 统计修复 —— 以及本版两项:事实性限额护栏与用量区间 / 时间序列。
✨ 新增功能
① 事实性限额护栏 —— 账号级预算闸门:用尽自动换号,而不是等上游返 402/429。
- 四条护栏:余额地板(
reserve_credits)· 当日 token(daily_token_limit)· 当日 credit(daily_credit_limit)· 单模型当日 token(model_daily_token_limit); - 三层作用域(global + realm 两级):留空 = 继承、显式 0 = 关闭;
- 后台余额刷新:每 tick 最多刷一个最陈旧账号(30min tick + 12h TTL),惰性启动(阈值全 0 时线程根本不存在,零副作用);
- 计数来自本地 usage 折叠 —— 不新增任何上游调用;
- 402 / 额度停放:停到 UTC+8 每日 10:00(qoder 自己的重置窗口,非通用 4am),仅余额恢复可提前解封;
- 临期积分优先分派:7 天内到期的额度优先消耗(smooth weighted round-robin),默认窗口 0(关);
- 面板:设置页限额表格(带量纲提示)+ 账号行四态徽标(未启用 / 未知 / 已暂停 / 正常)。
② 用量区间与时间序列:/usage/analytics · /usage/by-account · /usage/perf 支持 ?range=day|week|month|all|custom&since=&until=;新增 /usage/timeseries;realm=all 现在是真正的通配符(此前 /usage/perf 采样恒为 0)。
🐛 问题修复
- 限额设置此前不会生效(接线缺口):没有任何生产代码把 limits 推给账号池 —— 配置写进
settings.json就停住了,阈值恒 0、守卫永不触发。现在启动时与设置保存后各推一次(free 名单同批推送,避免空集拦住全部模型); - 后台刷新线程此前会静默死掉:
AccountPool.log默认实现只接受一个位置参数,而刷新线程传了level=→ 一 tick 就TypeError退出,整条刷新链路失效; - 设置页改阈值现在会打印状态翻转日志(此前只有其它路径打)。
🎨 体验优化:面板两处(限额表格的量纲提示、账号护栏四态徽标)—— 见上 ① 末条。
⚠️ 量纲说明(重要):qoder 的 credits 是积分点数(实测账号区间 0~1100),不是 token 量级 —— 照 token 量级填阈值会永远拦不到任何账号;- 默认全部关闭(0) —— 不给任何非零默认值;
- 参考材料:本仓
.team/_gap/(双向差距分析 6 份文档 + 实测证据)。
验证
- 全量 693 checks / 0 failed;
- 端到端(真实链路,非桩):配置 →
quota_enabled=True→ 后台线程启动 → 守卫命中 →pick返回 None → 网络层调用计数 0(确实没发上游)→ 复位后账号恢复; - 配置层 A1-A5 · 守卫层 B1-B6 · 汇合点 C1-C3 · 折叠 D2-D4 · 后台刷新 E1-E5(含惰性启动)· 派发偏好 F1-F6(含「默认关时与改动前等价」「永不过期占位不参与权重」两条边界)· 停放时钟 G1-G2 全部通过;
- 一条流程留痕:这条端到端判据前五轮不闭环,根因是实现侧接线缺失 + 测试侧判据位置错误各占一半 —— 判据应拦在网络层(是否真的出网),而不是「
open_upstream是否被调用」(网关正常路径本来就会进它、由它内部选号)。
v1.2.19
✨ 新增功能:无
🐛 问题修复
- usage 记录(与面板)的 credit 统计恒为 0(issue #22,by @jianjiuss):
_extract_usage()读的是单数usage.get("credit"),而上游给的是复数credits(附original_credits/billable)——usage.get("credit")恒为 None,于是所有 usage 记录都记成 0(报告者实测 426 条流式 + 16 条非流式全部为 0)。- 现在复数
credits优先、保留单数兜底(防其它区域字段差异); - 顺带记录
original_credits(参与聚合 —— 折扣时段对账要用)与billable(仅写进行、不进聚合 —— 布尔求和没有意义)。
- 现在复数
🎨 体验优化:无
- 历史数据不可回填:修复只影响此后写入的行;此前记录里的那个 0 是永久性的(当时的原始
credits从未落盘)。我们不会在日志或文档里暗示它可恢复。
验证
- 全量 657 checks / 0 failed([39] 段 10 条);
- 修复前的红色证据已留存(落盘前抓到的原始输出:复数
credits=97.96→credit: 0;两者都有时旧实现取单数)—— 没有它,这个回归网就是空转; - 变异自证 4 条:改回只读单数 → 必红;单数优先 → 必红;抽掉
original_credits→ 必红;把billable塞进聚合表 → 必红; - 面板不受影响(已 grep 确证:它读的是账号对象的
credits.remain、模型元数据的credits、签到侧的earned_credit,没有一处读 usage 行的credit); - 未覆盖(如实):
credits为字符串或None的形态(or链会把"0"当假值,属理论边界)。
v1.2.18
✨ 新增功能
GET /credits/summary:API-key 级鉴权的只读积分汇总(issue #21)——给下游集成(如网关池面板)展示各账号池余额用。- 鉴权走
_key_ok()(API Key 或面板会话均可,未设 Key 时放行),刻意不放进_is_panel_route()—— 否则就会要求X-Panel-Token,与用途相反; - 只返回本地已登记的
credits,绝不触发任何上游查询 —— 这是本 issue 的核心约束(对照GET /accounts/credits:那个会逐号fetch_credits()强刷,适合作者手动点、不适合机器轮询); - 返回
{ by_realm, totals, accounts[], note },每行含{uid, nickname, realm, credits_remain, credits_used, credits_size, registered_at}; - 新鲜度语义写在字段名与
note里:返回的是「上次登记时的值」(签到写权威值 + 每次消耗回写),不是实时余额; - 未登记过积分的账号照常列出但
credits_*/registered_at为null且不计入汇总;空池 /POOL=None返回空结构而非报错。
- 鉴权走
🐛 问题修复:无
🎨 体验优化:无
- README 的接口一览与说明补上该路由(含「为什么不能拿
/accounts/credits轮询」的对照)。
验证
- 全量 647 checks / 0 failed([38] 段 12 条);
- 命门:
fetch_credits()桩计数 = 0 —— 且这次用真实 handler 验证(ThreadingHTTPServer+ 后台线程 +urllib真请求,鉴权 / 路由匹配 / JSON 写出全走真代码路径); - 能红证据:把实现改成「先
fetch_credits()再返回」→ 命门断言必红(变异下桩被调 3 次 = 3 个账号各一次); - 鉴权矩阵:有效 key 200 / 无效 key 401 / 无凭据 401 / 面板会话 200 / 未设 key 放行 200(全部 0 次 fetch);
- 边界:空池 /
POOL=None/ 无 credits 账号 / 部分 credits 全绿。