Skip to content

fix(provider-routing): use current API eligibility and keep recovery polling timely - #8342

Merged
huangruiteng merged 3 commits into
mainfrom
codex/api-quota-fallback-20261108
Nov 10, 2026
Merged

huangruiteng merged 3 commits into
mainfrom
codex/api-quota-fallback-20261108

Conversation

@loopx-agent

@loopx-agent loopx-agent commented Nov 8, 2026 •

Copy link
Copy Markdown
Collaborator

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.mjs

Ruff/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.

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 loopx-agent left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

  1. [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 的时钟测试。
  2. [P2] 订阅额度读取期间 key 失效,仍会切进已知不可用的 API。 native.py:463 在外部订阅观测之前读取 key;观测返回后仍用旧布尔值。用真实 key store 在 observe 回调中写入最后一个 key 的结构化 quota 429,返回新鲜的订阅耗尽结果时,候选数已是零,当前提交仍执行了一次 API slot 切换。该场景是旧缺陷未覆盖的窗口。把候选读取放在外部观测之后、选择之前;若准备阶段还引入等待,在其有意义的效果边界复核。用同样的观测中失效夹具断言不准备 relay、不切换。
  3. [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.

@mergify

mergify Bot commented Nov 9, 2026

Copy link
Copy Markdown

This pull request has merge conflicts with main and cannot be merged
until they are resolved. Please rebase or merge the base branch, @loopx-agent.

Choose the remote for the base repository, not an out-of-date fork.
For a fork clone, first inspect git remote -v; upstream must point
to https://github.com/loopx-project/loopx.git. If it is absent, add it
with git remote add upstream https://github.com/loopx-project/loopx.git.
Then run:

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 HEAD

For a same-repository clone whose origin points to
https://github.com/loopx-project/loopx.git, use origin instead of
upstream for fetch/rebase. If you prefer merging the base, use
git merge <base-remote>/main and push normally.

Keep the DCO Signed-off-by trailer on every commit when you rebase.
https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork

@mergify mergify Bot added the needs-rebase Mergify: the pull request has merge conflicts with its base branch label Nov 9, 2026
…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>
@mergify mergify Bot removed the needs-rebase Mergify: the pull request has merge conflicts with its base branch label Nov 10, 2026
@huangruiteng huangruiteng changed the title fix: return from exhausted API fallback fix(provider-routing): use current API eligibility and keep recovery polling timely Nov 10, 2026

@huangruiteng huangruiteng left a comment •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@huangruiteng
huangruiteng merged commit 4842641 into main Nov 10, 2026
12 of 54 checks passed
@huangruiteng
huangruiteng deleted the codex/api-quota-fallback-20261108 branch November 10, 2026 04:47
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