Repository navigation
fix(provider-routing): use current API eligibility and keep recovery polling timely - #8342
Conversation
Treat API readiness as current key eligibility instead of configuration presence, and stop projecting an exhausted pool as active. Add recovery coverage for returning to a subscription while api_fallback remains enabled. Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com>
loopx-agent
left a comment
There was a problem hiding this comment.
Reviewer: model_agent, gpt-6.1-sol, OpenAI, runtime_reported, reasoning effort max
Request changes conclusion (author-owned PR; GitHub blocks formal self-review)
发现 / Findings
- [P2] 已选 API 路径耗尽后,自动回切订阅多等 4 分钟。 当两个订阅最近一次都显示耗尽、已经选中的 API 又没有可用 key 时,native_policy.ts:213 跌入通常给订阅使用的等待分支,重查间隔从 60 秒变为 300 秒。隔离 wheel 中,429 后订阅 B 已在原定重查前恢复:基线在第 60 秒回切,当前提交直到第 300 秒才回切;第 60 秒连订阅观测都被跳过。普通 429 的 30 秒 key 冷却到期后,观察器也仍显示不可用,直到第 300 秒才恢复报告并重新执行 relay 准备。保留准确的 all_unavailable,在已选 API 且 fallback 仍启用时维持原有 60 秒重查上限,允许更早的订阅重置唤醒。请加入不调用 Refresh 的时钟测试。
- [P2] 订阅额度读取期间 key 失效,仍会切进已知不可用的 API。 native.py:463 在外部订阅观测之前读取 key;观测返回后仍用旧布尔值。用真实 key store 在 observe 回调中写入最后一个 key 的结构化 quota 429,返回新鲜的订阅耗尽结果时,候选数已是零,当前提交仍执行了一次 API slot 切换。该场景是旧缺陷未覆盖的窗口。把候选读取放在外部观测之后、选择之前;若准备阶段还引入等待,在其有意义的效果边界复核。用同样的观测中失效夹具断言不准备 relay、不切换。
- [P2] 合并后的 wheel 仍叫 0.15.4,普通升级会保留旧路由。 pyproject.toml:7 与 native manifest 使用的 0.15.4 已被 #8394 合入 main。当前 main 与无冲突试合并树都产出同名 0.15.4 wheel,但实际观察器不同。隔离安装 main wheel 后,pip install --upgrade 指向试合并 wheel 返回同版本已安装,读回仍走旧的不可用 API 接续。新建不可变环境可直接装入新产物,但无法按版本区分两者。请基于最新 main 同步分发及两份 manifest 版本,重跑从 main wheel 到新 wheel 的安装和路由读回。
动机 / Motivation
开启 API 接续的 Codex 用户在订阅耗尽后,希望继续已有会话,并在订阅恢复时自动返回。旧版把配置了 key 当成 API 可用:所有 key 已冷却时仍能切到 API,状态也会继续说正在使用 API。当前提交能拒绝切到已经确认无可用 key 的 API,并在已选 API 不可用时给出准确的等待状态。此评审范围是路由资格、后台恢复和安装读回;真实扣费、正在运行的请求回放和新账号权限不在范围内。仍需补齐自动恢复时序、观测期间的 key 变化以及升级产物的版本区分。
改动思路 / Approach
沿用现有分工:Python 读私有 key 状态和订阅观测,TypeScript 决定路由,native owner 继续负责切换。当前 PR 应完成候选资格和不可用状态修正,同时保持已有回切时序,并交付可区分的安装版本。保留已合适的 key 候选 owner;只修改状态标签会让进入不可用 API 的旧行为继续发生。把可用性和下次观察时间分开,避免为显示等待状态而放慢恢复。订阅优先、手动选择、现有重启和空闲条件都继续归原 owner。
具体改动 / Changes
关键代码讲解
- NativeAccounts.tick 由查看有没有已存 key 改为查看当前候选。无已知可用候选时不会正常进入 API;但它在读取订阅状态前就冻结了这一判断。
- decide 先选择已恢复订阅,再处理已选 API。API 不可用时走已有的 all_unavailable,这同时改变了 retry_at。原有账号选择结果、开关和等待字段仍在同一个状态对象中。
- Watcher._observe_once 按这个时间唤醒。受保护的 tick 会在 retry_at 到期前直接返回旧投影,因此即时刷新测试可以通过,后台仍错过原有的回切时间。App 已有相应文案,显示等待与显示当前所选槽位是两件事。
规范对照:packages/loopx-codex-provider-routing/NATIVE.md,版本 5f78e87。固定版本。NATIVE-FALLBACK-AUTHORITY 已覆盖独立 opt-in 和新鲜订阅耗尽;NATIVE-SUBSCRIPTION-PRIORITY 已覆盖新鲜可用订阅在选择时优先,后台发现该状态的时序退化见发现 1;NATIVE-REQUEST-BOUNDARY 沿用原 relay 的请求归属和不重放保证。NATIVE-INSTALLED-PROVIDER 的独立 wheel 和 doctor 可运行,升级版本冲突见发现 3。
对主干的风险 / Risks
| 同一组隔离观测 | 原基线 | 当前提交 |
|---|---|---|
| 初次准备 API 时,唯一 key 已知耗尽 | 切到 API | 留在订阅槽位,未准备 relay |
| API 已选,429 后订阅恢复 | 60 秒回切 | 300 秒回切 |
| 订阅观察中途写入最后一个 key 的额度拒绝 | 切到 API | 仍切到 API |
| 未启用 API 接续或订阅证据未知 | 无 API 切换 | 无 API 切换 |
PASS:17 项 TypeScript policy 测试,70 项 Python 测试及 28 项子测试;从基线、当前提交、main 和试合并树分别安装的 wheel 均通过 doctor。对照调用实际的 Watcher、native TS bridge、私有 key store 和 wheel;原生槽位及额度响应使用合成 transport,未访问真实账号或付费模型。FAIL:后台重查时序、观察期间已知拒绝和同版本升级读回。UNVERIFIED:按当前策略未观察远端 CI,未启动实际 Codex App 或真实模型请求。
语义与 CI 对齐
复用既有 all_unavailable 合适,但它不能把已选 API 的重查从一分钟无提示地拉长到五分钟。将 key 判断留在现有 owner,保留原恢复边界,用普通 tick 和受控时钟重跑两个 429 分支。只在隔离试合并产物中把取样移到订阅观测之后、保留 60 秒上限并改用 0.15.5,对照分别恢复了拒绝、回切和升级结果;这些修复未出现在评审提交。
我的整体评价 / Verdict
应该保留基于候选的路由判断与准确状态。当前提交让自动恢复变慢,仍能在一个可复现的观测窗口切错路由,并且无法通过普通同版本升级送达已有安装,因此请求修改。检查了 App 的现有状态消费者和标签;没有布局变化,本次未做浏览器视觉检查。现有状态名和权限边界保持原样,无需再加一套状态或配置。修复后请用不触发手动刷新、无真实扣费的三组对照复验,再核对新提交。
English verdict: REQUEST_CHANGES at a887e6f. Preserve timely recovery, recheck key eligibility after subscription observation, and ship a distinct upgrade version.
|
This pull request has merge conflicts with Choose the remote for the base repository, not an out-of-date fork. git fetch upstream
git rebase upstream/main
# Resolve each conflict, git add the resolved files, then git rebase --continue.
git push --force-with-lease origin HEADFor a same-repository clone whose Keep the DCO |
…pair-20261110 Signed-off-by: huangruiteng <huangrt01@163.com> # Conflicts: # packages/loopx-codex-provider-routing/pyproject.toml # packages/loopx-codex-provider-routing/src/loopx_codex_provider_routing/native-extension.toml
… waits Signed-off-by: huangruiteng <huangrt01@163.com>
There was a problem hiding this comment.
Reviewer: model_agent, gpt-6.1-sol, OpenAI, runtime_reported, reasoning effort max
动机 / Motivation
显式启用 Codex provider fallback、在订阅和 API 配额之间等待恢复的用户。 API 池全部耗尽时原逻辑仍把“存了 key”当成可用;修复后若退回订阅默认300秒轮询又会推迟恢复。现在外部 observe 后读取真实候选 key,切换前再核验,并在 API 活跃不可用时保持60秒以内观察。
真实 key store 和隔离 Host 适配器中,永久耗尽返回 all_unavailable;临时429在普通后台 tick恢复,订阅恢复会优先切回;observe/prepare 期间失效的 key 不导致 enroll/switch。
本 PR 不验证账户余额、不自动开启付费 fallback、不重发失败请求,也不上传或记录 key。
改动思路 / Approach
Python 只负责 key store 与 Host IO,TS decide 仍拥有优先级和 retry_at。候选读取放在可能很慢的外部 observe 之后,_prepare_api 在 relay prepare 前后复验,消除已知失效仍切换的窗口。API 不可用时保留既有短观察节奏,并与更早的订阅 reset 取最小值。包、扩展清单和 native observer 版本统一升0.15.6,确保升级实际落地。
本次仅修已启用 fallback 的资格读取、切换前复验和后台恢复节奏;不引入付费探测或新的账号调度模式。
入口为 native account watcher tick 与已有 configure/status;权威输入来自 已授权 slots/fallback、observe 当前订阅、实际 API candidate pool,由 native_policy.decide 决定,结果交给 native status、retry_at、当前 selection。ready 从当前候选池即时推导,不能再把 key 存在当人工同步状态;不增加授权或持久 ready 标记,policy 仍在 TS。 相同领域的可逆精简已经在本次完成;没有把职责搬成另一套平行规则。
具体改动 / Changes
关键代码讲解
tick:外部 observe 完成后再采样 candidates,将当前可用性交给 typed policy,而非把已存 key 等同可用。 输入来自已授权 slots/fallback、observe 当前订阅、实际 API candidate pool,下游为native status、retry_at、当前 selection;失败由无可用候选保持不可用并短重查;relay/key失效拒绝切换处理。
_prepare_api:relay 准备前后两次核对候选;无候选不 enroll/switch,使用既有 api_relay_unavailable。 输入来自已授权 slots/fallback、observe 当前订阅、实际 API candidate pool,下游为native status、retry_at、当前 selection;失败由无可用候选保持不可用并短重查;relay/key失效拒绝切换处理。
decide:保留订阅优先和显式 fallback,API 活跃但耗尽的等待上界60秒,并尊重更早 reset。 输入来自已授权 slots/fallback、observe 当前订阅、实际 API candidate pool,下游为native status、retry_at、当前 selection;失败由无可用候选保持不可用并短重查;relay/key失效拒绝切换处理。
规格依据:packages/loopx-codex-provider-routing/NATIVE.md,固定修复前版本 e7ae4b077c0a2da0791753c9be3f7e4ff0b59b6e。映射标签 explicit-fallback-boundary:fallback 只有显式启用且在允许的请求/选择边界才生效;订阅优先,失败请求不自动重发,现有权限仍适用。 —— implemented,验证来自公开测试入口 uv run --extra test python -m pytest -q tests/presentation/test_openai_fallback.py; node --test packages/loopx-codex-provider-routing/smoke/native-policy.test.mjs。该标签是本次评审的映射名,不伪称规格原有编号。
差异形态:production: 5 files +23/-10; tests_or_fixtures: 2 files +78/-0; docs: 1 files +7/-0; generated: 0 files +0/-0; mechanical_moves: 0 files +0/-0。fallback=false 时即便安装扩展并有 key 也不会选择 API;manual target、Host能力和既有订阅排序仍独立检查。
对主干的风险 / Risks
candidate 是本地最新资格,不是上游余额保证;即使准备前后复验,通过后也仍可能遭真实服务拒绝,后续请求继续由原 relay 错误分类处理。60秒 tick是观察上界而非强制请求或持续付费探测。升级版本必须一致,否则源代码测试通过也无法让已安装用户获得修复。
正向:普通 tick 在429 cooldown结束后重新选到 API;订阅先恢复则回订阅,不要求用户手动 refresh。 负向与恢复:observe 或 relay prepare 期间 key 被禁用时拒绝切换;全部永久耗尽不再显示 api_active;关闭 fallback 即使 key存在也不选择 API。
验证:73 Python 项与28 subtests、17 Node policy 项通过。隔离 venv 真实 wheel 由0.15.5升级0.15.6,site-packages 导入后同输入由错误 api_active 变为 all_unavailable,retry_at 保持1060。 Ruff/diff hygiene 与 exact-head canary direct/run checks 无失败;manual_hold_count=0。手工 benchmark/authority 判断由本次完整评审执行,不把该 hold 写成自动通过。 历史失败保留为反例,不把新 head 的通过反写成历史通过。GitHub CI 与合并策略是单独的 readiness 判断;本结论不从红绿状态推导代码正确性。
上游 observe 与付费请求使用隔离适配器,未真实切换个人账户或调用付费模型;wheel升级与本地 key store/TS桥接为真实路径。
合并读回:该精确 head 已合入,merge commit 为 484264103ea8678e0cab5303d565a40b1fcaff5e。合并前 readiness 为 ready=false,GitHub 检查汇总 {"pending": 3, "success": 8, "failure": 16};review conclusion 有效、线程完整且 unresolved=0。逐个读取原始 Checks 确认,汇总里的相关红项来自旧 workflow 的 CANCELLED,而不是这些 job 执行了失败断言;保留取消和未执行的区别。旧 merge-gate 因所属 changes job 被取消而报告 classification did not succeed;替代 run 当时尚未完成,不能宣称整套 CI 通过。 依据维护者对本批“自修复自合并”的明确授权使用 admin bypass 处理策略/CI 等待;必需本地验证、已修复发现的核对与精确版本评审单独完成。
我的整体评价 / Verdict
最终评审版本为 c158b438e98546c51c59f60a15f4629825619aef,检查覆盖整个剩余 diff 与当前调用方,不仅是最后一条 comment。真实 key store 和隔离 Host 适配器中,永久耗尽返回 all_unavailable;临时429在普通后台 tick恢复,订阅恢复会优先切回;observe/prepare 期间失效的 key 不导致 enroll/switch。 本次仅修已启用 fallback 的资格读取、切换前复验和后台恢复节奏;不引入付费探测或新的账号调度模式。 上述已有缺陷均有入口级反例与恢复读回,未发现仍需阻止本次交付的具体问题。
已检查 packages/loopx-codex-provider-routing/src/loopx_codex_provider_routing/native.py 及现有消费者,本 PR 不改变渲染布局或内容层级;本次验证集中于入口、状态与恢复。 没有对已安装 App、真实个人账号或付费模型作额外观测。
本 PR 不验证账户余额、不自动开启付费 fallback、不重发失败请求,也不上传或记录 key。 合并仍需该版本的远端 head、未解决线程、旧阻塞评审与 readiness 读回;本评审不自行赋予合并或付费执行权限。
English verdict: APPROVE. Head c158b43. The bounded fixes preserve existing authority and recovery contracts; the stated untested boundaries are not claimed as verified.
Problem and outcome
显式启用 Codex provider fallback、在订阅和 API 配额之间等待恢复的用户。 API 池全部耗尽时原逻辑仍把“存了 key”当成可用;修复后若退回订阅默认300秒轮询又会推迟恢复。现在外部 observe 后读取真实候选 key,切换前再核验,并在 API 活跃不可用时保持60秒以内观察。
真实 key store 和隔离 Host 适配器中,永久耗尽返回 all_unavailable;临时429在普通后台 tick恢复,订阅恢复会优先切回;observe/prepare 期间失效的 key 不导致 enroll/switch。
Design and scope
Python 只负责 key store 与 Host IO,TS decide 仍拥有优先级和 retry_at。候选读取放在可能很慢的外部 observe 之后,_prepare_api 在 relay prepare 前后复验,消除已知失效仍切换的窗口。API 不可用时保留既有短观察节奏,并与更早的订阅 reset 取最小值。包、扩展清单和 native observer 版本统一升0.15.6,确保升级实际落地。
本次仅修已启用 fallback 的资格读取、切换前复验和后台恢复节奏;不引入付费探测或新的账号调度模式。
本 PR 不验证账户余额、不自动开启付费 fallback、不重发失败请求,也不上传或记录 key。
Validation
73 Python 项与28 subtests、17 Node policy 项通过。隔离 venv 真实 wheel 由0.15.5升级0.15.6,site-packages 导入后同输入由错误 api_active 变为 all_unavailable,retry_at 保持1060。
Reproduction:
uv run --extra test python -m pytest -q tests/presentation/test_openai_fallback.py; node --test packages/loopx-codex-provider-routing/smoke/native-policy.test.mjsRuff/diff hygiene 与 exact-head canary direct/run checks 无失败;manual_hold_count=0。手工 benchmark/authority 判断由本次完整评审执行,不把该 hold 写成自动通过。
上游 observe 与付费请求使用隔离适配器,未真实切换个人账户或调用付费模型;wheel升级与本地 key store/TS桥接为真实路径。
Reviewed head:
c158b438e98546c51c59f60a15f4629825619aef. Historical timing claims or failures are not reused as evidence for an unmeasured final implementation.