Skip to content

feat(gateway): filter accounts by per-model qualification before sticky selection - #492

Merged
qxcnm merged 8 commits into
qxcnm:mainfrom
shiziyang2000-bit:fix/account-model-qualification
Oct 10, 2026
Merged

qxcnm merged 8 commits into
qxcnm:mainfrom
shiziyang2000-bit:fix/account-model-qualification

Conversation

@shiziyang2000-bit

@shiziyang2000-bit shiziyang2000-bit commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

明确“不支持本模型”的账户此前仍会被粘性路由和轮转反复选择,产生可避免的 400。本 PR 按账户和实际发送的模型记录短期资格拒绝,并在候选过滤、粘性选择和轮转前使用记录。

  • 只接受完整 JSON 的明确 error.message 模板,核对被拒绝模型与最终发送模型;工具/图片模型、参数错误及不明确的文案不污染主模型资格。
  • 候选查询、别名转换和实际请求使用同一模型解析。全部候选均被标记时保留原列表,让最后一个候选的原始错误仍可交付并学习。
  • 400 分类预读有 64 KiB、最多 2 秒空闲和 10 秒总等待限制;超限、超时、读错误均不学习,消费的前缀可回放,继续既有失败切换/交付流程。
  • 精确的资格拒绝只记账户×模型;工具/其他模型拒绝沿用普通 400 处理。
  • 恢复资格以最终完成为依据:HTTP 200 后失败、断流或 completed 事件内的失败状态不清记录,不建立成功 response_id 绑定;正常文本和工具请求成功才清除。
  • 原生 Responses sidecar 区分 MCP failed/error 的局部错误与整个响应失败;真正的 response.failed/error/canceled、响应错误字段和失败状态仍优先,并保持透传字节不变。新增实际 HTTP bridge 与资格恢复回归。
  • TTL 默认 1800 秒,零/非法值回退默认,缓存仅当前进程有效;已接入现有环境变量目录并补中英文说明。
  • 联合测试发现主线四处 revision 10 旧模型数量断言。在当前 main 逐项复现后更新图片专项、目录顺序与价格测试,增加 gpt-6.1-sol 两档四类价格及目录排序检查。

验证覆盖两种 OpenAI 风格端点的流/非流、tools、别名、sticky、最后一个候选、全部被标记、TTL 与大/挂起/持续发送/读取失败的 400,以及可恢复 MCP 失败和真正响应失败后的资格状态。格式及 diff 检查通过;最终完整工作区和最新提交 CI 结果见维护者评论。验证使用本地隔离上游,没有使用真实账号或生产请求。
CI 还触发了既有 busy_database 测试的计数发布与内存预算释放时序竞态。测试现在用已有的 20 秒有界等待,同时确认写入完成、queued_bytes 和 queued_jobs 归零;数据库写入、busy retry、零丢失和内存释放断言全部保留,没有改动生产实现或 CI 配置。最终分支已同步包含 #464、#487、#491 的当前 main。

@shiziyang2000-bit

Copy link
Copy Markdown
Contributor Author

您好,感谢维护。

这个 PR 处理的是「账户不支持某个模型」时的反复 400:上游会返回 The '<model>' model is not supported when using Codex with a ChatGPT account,现在会被当成普通 non-200 继续换账户重试,用户看到反复的终止性 400(典型路径:支持该模型的账户偶发 502 → 冷却 → 轮到不支持该模型的账户 → 400)。

  • 改动:新增 (账户×模型) 不支持记忆(TTL 默认 30 分钟,CODEXMANAGER_ACCOUNT_MODEL_UNSUPPORTED_TTL_SECS 可覆盖;只匹配“模型不被支持 + 账户/Codex 范围”的文案,参数类 400 不会误记);400 正文精确分类;候选过滤先于粘性/轮转(绑定指向不支持账户时仅对该模型跳过);同账户同模型一旦成功即清除记忆;未命中该模式时行为不变。
  • 单测覆盖:跨模型隔离、TTL 到期、成功清除、负样本;account::model_support 7/7、gateway:: 814/814、gateway::upstream:: 251/251。
  • 隔离装置对照:修复后第二次请求不再尝试该账户(gateway_candidate_model_filter … dropped=1),原版则会再次撞同一个 400。

想麻烦看下能否合入并随下一个版本发布?另外 #488(本地上限可配置)已合并,感谢。

@qxcnm

qxcnm commented Oct 3, 2026

Copy link
Copy Markdown
Owner

这个改动有实际价值:按账户和模型记住明确的资格拒绝,并在粘性选择、轮转前过滤,可以减少同一模型反复撞到不支持账户的 400。当前先暂缓合并,下面三处需要补齐;审查基于 head fde4291808938a536798445630f345b7119c3e78。

  1. 拒绝文案中的模型没有与实际请求模型核对,会误记资格。

    account_model_support.rs:69–84 只判断全文包含 model、不支持、账户/Codex 关键词,141–149 随后直接标记传入的模型;candidate_executor.rs:726–731 传入的是当前 attempt_model_for_log。例如项目已有 Images 兼容链路使用主模型 gpt-6-luna 加 image_generation 工具模型 gpt-image-2,如果上游返回 The 'gpt-image-2' model is not supported when using Codex with a ChatGPT account,当前实现会把该账户的 gpt-6-luna 标为不支持,随后连纯文本请求也被过滤 30 分钟。参数或工具能力错误只要同时包含这些关键词,也存在同类风险。

    建议解析显式 error.message / 错误代码,以明确模板提取被拒绝的模型,并与本次实际发送的模型核对;别名需要按发送到上游的最终模型统一查询和写入。无法确认模型维度时保留普通 400 行为,不写资格缓存。图片工具模型或功能不支持,不应标记主对话模型不支持。

  2. 新增的 400 正文读取没有大小或等待上限,会阻塞失败切换。

    stream_preflight.rs:368–395 对所有 400 调用 into_buffered_async(),底层 response.rs:114–123 持续把数据加入 Vec 并等待 EOF;该分支没有使用现有 preflight 的字节、idle、wall-clock 限制。后续的 bound_message() 只裁剪已读取完的文本,无法限制读取本身。transport.rs:552–568 明确不对 JSON/SSE 错误响应执行 fast-close,所以流式请求收到 JSON 400 后正文迟迟不结束时,可能延迟或阻塞当前尝试的失败切换;持续发送正文还会扩大内存占用。原实现有更多候选时可以直接进入 StatusFailover。

    建议复用有界 prefix 预读,设置字节上限、idle timeout 和总等待上限,只用完整且可确认的错误内容学习资格。超限、超时和读取失败需要明确的失败切换/透传行为,同时保持未匹配的最后一个候选响应可交付;不能为分类等待无界 EOF。

  3. HTTP 2xx 不等于模型请求最终成功。

    candidate_executor.rs:815–819 在响应交付前仅凭 HTTP 状态清除记录。SSE 可以先返回 HTTP 200,之后发送 response.failed 或中途断开;尤其最后一个候选的 preflight 会直接放行响应,这时还没有成功证据。

    建议把清除操作放到确认响应完成的结果处理处:非流式确认没有显式失败,流式确认收到有效的成功终止事件。当前异步响应交付具有完成回调,应在确认最终结果后清除,避免仅收到响应头就恢复资格。

回归测试需要覆盖:主模型与图片工具模型不同、参数错误含上述关键词、模型别名的一致性;两个账户先拒绝后过滤、sticky 绑定、所有候选均被标记、最后一个候选仍能学习、TTL 到期;400 正文挂起/持续发送/超大正文/读取失败;HTTP 200 后 response.failed、断流和真正成功。请同时覆盖 /v1/responses、/v1/chat/completions 的流式与非流式请求,以及带工具的请求。新 TTL 环境变量也需要补运行配置文档,并接入现有环境变量目录,说明默认值、非法值回退、作用范围及缓存仅在当前进程内生效。

当前 Rust CI 失败停在 cargo fmt --all -- --check,具体是 stream_preflight.rs 和 candidates.rs 的格式;修完上述逻辑后建议运行:

cargo fmt --all -- --check
cargo test -p codexmanager-service --lib account::model_support -- --test-threads=1
cargo test -p codexmanager-service --lib gateway::upstream:: -- --test-threads=1
cargo test -p codexmanager-service --lib gateway::http_bridge:: -- --test-threads=1
cargo test --workspace -- --test-threads=1

以上结论来自源码和当前 CI 审查;没有使用真实账户,也没有对真实上游执行实测。作者提供的隔离上游验证说明覆盖了基础路径,但没有给出这些边界的验证证据。

@qxcnm

qxcnm commented Oct 10, 2026

Copy link
Copy Markdown
Owner

已直接补齐修复并推送到原 PR 分支,最终审查提交为 72df74b5f8437ce2fffefade508b1b0e40a57f14,已同步包含 #464、#487、#491 的当前 main。

这项改动能减少已知不支持某模型的账户被重复选中,修复后有实际收益,审查通过。

  • 资格拒绝只接受完整 JSON 中明确的 error.message 模板,并核对与最终发送模型一致;参数、工具及图片模型错误不会污染主模型。别名、候选过滤和粘性路由使用同一模型解析。
  • 400 分类预读限制为 64 KiB、2 秒空闲、10 秒总等待;超限、超时或读取失败不学习。最后一个候选保留原状态、headers 和正文,全部被标记时仍能交付具体上游原因。
  • 仅确认成功完成后清除资格拒绝记录;HTTP 200 后失败、断流或 completed 内携失败状态均不恢复资格。原生 Responses sidecar 区分 MCP 局部失败与整个响应失败,真正的响应错误仍优先。
  • TTL 默认 1800 秒,零值及非法值回退默认;已加入现有环境变量目录和中英文文档,明确进程内缓存及重启清空行为。
  • 联合测试复现并修正 main 上四处 revision 10 旧目录断言,补充 gpt-6.1-sol 价格及排序检查;既有 busy_database 测试使用有界等待同时确认写入计数和队列预算释放,全部原断言保留,未改生产实现或 CI。

验证记录:

  • 最终 head 的 CI 三项全部 SUCCESS:Rust、Frontend/Web、macOS arm64。
  • 本地组合 71ba5f43 的 cargo test --workspace --locked -- --test-threads=1 实际退出 101:2645 通过、4 失败、19 ignored。四项失败均为验证工作区缺少 apps/out;确认前端源码与已验证的 fix(settings): make gateway auth cleanup configurable with compatible defaults #464 完全一致后复用其静态产物,随后 Web 全包 36/36、rusqlite 全包 15/15、workspace doc 检查均退出 0。去除重复计数后,本地各组共覆盖 2664 项通过。
  • 最新组合 7761de4b 与最终 PR head 的全部受版本控制文件一致;相对前一组合仅追加 busy_database 测试的同步修复。cargo test --locked -p codexmanager-service --lib gateway::request_log_payload:: -- --test-threads=1 追加回归 37/37,退出 0;格式和差异检查均通过。
  • 19 项既有 ignored 中,14 项需隔离 MySQL/PostgreSQL,4 项需外部在线探测,1 项需明确配置隔离的真实网关数据库后端,未将这些项目记为通过。

回归覆盖 /v1/responses 与 /v1/chat/completions 的流/非流、tools、资格过滤与恢复、别名、sticky、最后候选、TTL 和异常 400。使用隔离本地上游及实际 HTTP bridge,未调用真实模型或使用生产账户。

可以合并。

@qxcnm
qxcnm merged commit 9d03ca8 into qxcnm:main Oct 10, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants