Skip to content

[Feature]: OpenRouter — automatic key rotation & model failover when the free-tier quota is exhausted #4024

Closed

Description

@nordz0r

Area

Provider adapters

What are you trying to accomplish?

Use OpenRouter free-tier models (openrouter/minimax/minimax-m3:free, openrouter/openrouter/free, etc.) behind a combo/failover target without manual intervention when a key's weekly free quota is exhausted (OpenRouter replies with 429 rate_limit_error, Weekly/Monthly Limit Exhausted... will reset at <date>).

What prevents this today?

OCX already has two related mechanisms:

  1. apiKeyPool multi-key failover (src/providers/key-failover.ts): on a 429, rotateKeyOn429() moves to the next key in the pool and puts the exhausted key into cooldown (default 60 s, cap 10 min, Retry-After respected).
  2. Combo failover across targets: a combo can fail over to another provider/model.

The gap for the OpenRouter free-tier case specifically:

  • The free-quota reset horizon is weekly (Your limit will reset at 2026-09-09 03:30:06), but MAX_COOLDOWN_MS caps a key's cooldown at 10 minutes. After 10 minutes the exhausted key becomes active again, and roughly every other request goes to a dead key, failing with 429 and only then rotating — the wasted attempt repeats all week until the quota actually resets.
  • The OpenRouter error body carries the exact reset timestamp (will reset at <date>), but the cooldown logic appears to only honor the Retry-After HTTP header; the body-parsed reset date is not used to extend the cooldown.
  • There is no per-key quota-state notion (e.g., exhausted_until: <ts>), so an exhausted key cannot be parked for the rest of the window the upstream itself declares.

What should OpenCodex do?

  1. Parse the OpenRouter 429 JSON body for rate_limit_error and extract the reset timestamp from the message; park the key in cooldown until that timestamp (bounded, e.g. up to 7 days, configurable).
  2. Optionally: a generic quotaExhaustedCooldownMs / quotaExhaustedUntilReset: true knob under retryOn429 (or a sibling policy) so other providers that report reset windows in the error body get the same behavior.
  3. With a single key in the pool, the same detection should feed combo-level failover: mark the target as quota-exhausted so the combo routes to the next target for the remainder of the window instead of burning one failed attempt per request.

Example usage or interface

{
  "providers": {
    "openrouter": {
      "authMode": "key",
      "apiKeyPool": [
        { "id": "key-a", "key": "sk-or-..." },
        { "id": "key-b", "key": "sk-or-..." }
      ],
      "retryOn429": {
        "enabled": true,
        "quotaResetFromBody": true,
        "maxQuotaCooldownMs": 604800000
      }
    }
  }
}

Expected behavior: after the upstream answers 429 Weekly/Monthly Limit Exhausted ... will reset at 2026-09-09 03:30:06, key-a is parked until that timestamp and all traffic goes to key-b (or the combo's next target) without per-request 429 tax.

Alternatives or workarounds

  • Manually pausing/removing the exhausted key in the dashboard until the reset date.
  • Using a short combo retry budget so every request pays one 429 before failing over (current de-facto behavior; wasteful on long windows).
  • External cron that swaps keys in the config when the weekly reset hits.

Checks

  • I searched existing issues and documentation.
  • This request describes a concrete OpenCodex workflow rather than merely naming a desired technology.
  • I removed secrets and personal data.

Activity

  1. added
    enhancementNew feature or request
    account-poolOAuth, credentials, Codex pool, quota, failover, plans
    providerProvider adapters, OpenAI-compat presets, upstream API quirks
    on Sep 8, 2026
  2. Ingwannu commented on Sep 8, 2026

    @Ingwannu
    Owner

    Thanks — I checked dev@29bb221c3. The bounded-cooldown limitation is real: src/providers/key-failover.ts clamps Retry-After to ten minutes, defaults to 60 seconds when the header is absent, and rotateKeyOn429 receives a header rather than an error-body reset field. A pool with fewer than two entries returns without recording a key cooldown. The proposed quotaResetFromBody/maxQuotaCooldownMs settings are not implemented today.

    One important distinction: rotation persists the selected replacement key. Expiry of another key's cooldown does not by itself switch back to it, so the code does not establish "every other request" failing while a healthy replacement remains selected. We need to distinguish key-local quota, account-wide quota, and a model/provider-specific limit before parking either a key or an entire combo target.

    Please share only the upstream hostname (no full URL/query), exact model ID, sanitized error type/message containing the reset time, whether Retry-After was present and its value, and whether the pool keys share one upstream account. Remove keys, account IDs and unrelated payloads. A reset time without an explicit timezone must not be interpreted as the OCX host's local time.

    I am keeping this as a feature/design issue, not marking it fixed. A safe direction is to respect a verified quota scope and reset deadline, then use an already-authorized fallback or surface exhaustion; rotating keys must not be treated as bypassing a shared account allowance. I am not proposing a global increase to the ordinary transient-429 cooldown or arbitrary parsing of every provider's error text.

  3. lidge-jun commented on Sep 8, 2026

    @lidge-jun
    Owner

    리뷰 · 우선순위 52 / 80

    이 이슈는 OpenRouter 무료 티어 주간/월간 쿼타가 바닥났을 때, OCX가 키를 너무 빨리 다시 집어 들어서 요청마다 429 세금만 내는 문제를 고쳐 달라는 요청이다. 작성자가 쓰는 모델은 openrouter/minimax/minimax-m3:free, openrouter/openrouter/free 같은 free 계열이고, 업스트림은 rate_limit_error와 함께 Weekly/Monthly Limit Exhausted... will reset at <date> 문구를 준다. 원하는 결과는 단순하다. 그 reset 시각까지 키(또는 콤보 타깃)를 주차장에 두고, 풀의 다른 키나 콤보의 다음 타깃으로만 보내라.

    지금 dev HEAD 29bb221c3의 src/providers/key-failover.ts를 다시 읽으면, 작성자 진단의 핵심은 맞다. MAX_COOLDOWN_MS가 10분으로 고정돼 있고, parseRetryAfterMs / rotateKeyOn429는 HTTP Retry-After 헤더만 본다. 본문 날짜는 안 읽는다. 헤더가 없으면 기본 60초 쿨다운이다. 풀 항목이 2개 미만이면 키 쿨다운을 기록하지 않고 null을 돌려, 호출 쪽이 429를 그대로 올린다. 그래서 “주간 reset인데 10분마다 죽은 키를 다시 시도”하는 체감은 실제로 난다. 다만 OWNER(Ingwannu) 댓글이 이미 중요한 교정을 했다. 회전이 선택한 대체 키를 persist하면, 다른 키 쿨다운이 끝나도 자동으로 죽은 키로 돌아가며 “요청마다 번갈아 실패”하는 그림과는 다르다. 그래서 설계를 짜려면 키 단위 쿼타 / 계정 단위 쿼타 / 모델·프로바이더 한도를 먼저 갈라야 한다. 공유 계정 허용량을 키 로테이션으로 우회하는 구현은 받으면 안 된다.

    점수 52인 이유. 무료 OpenRouter 풀 사용자에게는 실사용 통증이고, account-pool·provider 라벨과도 맞다. 그런데 지금 dev가 최적화하는 tip은 릴리스 기록 마감(#4021) 이후 제품 스택(#4002 등)과 하드락/쿼타 회귀 쪽이다. 이 이슈는 새 설정 면(quotaResetFromBody / maxQuotaCooldownMs)과 프로바이더별 본문 파서가 필요한 설계 이슈이지, 즉시 머지할 한 줄 패치가 아니다. #3376(reset window를 스케줄 입력으로), #3375(OAuth pool lifecycle), 이미 닫힌 Go 풀 #2060과도 이웃이지만 중복은 아니다. OWNER가 요청한 재현 패킷(호스트명만, 모델 ID, 살균된 에러, Retry-After 유무, 키가 같은 업스트림 계정인지)이 아직 이슈에 안 붙어 있다. timezone 없는 reset 시각을 호스트 로컬로 읽으면 안 된다는 주의도 이미 적혀 있다.

    라인 / 경로로 보면 문제 지점은 이렇다.

    라인 26 (src/providers/key-failover.ts) - MAX_COOLDOWN_MS = 10 * 60_000. 주간 free 한도를 표현할 수 없다
    라인 57–75 (src/providers/key-failover.ts) - Retry-After만 파싱하고 상한을 10분에 자른다. 본문 will reset at는 경로가 없다
    rotateKeyOn429 / 풀 크기 가드 - 키가 하나면 쿨다운을 남기지 않고 null. 콤보 failover로 “타깃 고갈”을 넘기는 연결도 없다
    설정 스키마 - 제안된 retryOn429.quotaResetFromBody / maxQuotaCooldownMs는 현재 config/types에 없다. 넣으면 프로바이더 공통 정책인지 OpenRouter 전용 파서인지부터 정해야 한다
    이슈 본문 “every other request” 서술 - persist된 선택 키가 있으면 그 서술이 과하다. 재현 시 selected key / cooldown map을 같이 적어 달라고 OWNER가 이미 요청했다

    메인테이너의 판단이 필요한 지점

    • OpenRouter 본문 파서를 어댑터 전용으로 둘지, retryOn429 공통 훅으로 열지
    • 쿨다운 대상을 키만으로 할지, 콤보 타깃·모델 단위까지 올릴지(계정 공유 키면 키 로테이션 금지)
    • 최대 park 기간 상한(예: 7일)과, timezone 없는 timestamp 거절 규칙을 어디에 문서화할지
    • 2.49에서 받을지, 설계 이슈로 남겨 재현 패킷·스코프 확정 후 별도 PR로 받을지

    너의 추천
    enhancement로 유지하고, 지금 바로 구현 PR을 받지 마세요. OWNER 댓글대로 살균된 재현 패킷(호스트명, 모델 ID, 에러 type/message, Retry-After, 키들이 같은 계정인지)을 이슈에 붙인 뒤, “검증된 쿼타 스코프 + reset 마감 → 이미 인가된 fallback 또는 고갈 표시” 설계만 확정하세요. 일반 transient 429의 10분 상한을 전역으로 올리는 패치나, 임의 프로바이더 에러 문자열 파싱은 거절하는 편이 맞다. 구현 시에는 key-failover.ts에 body-reset 경로를 명시적으로 넣고, 공유 계정 키면 rotate 대신 타깃 고갈로 올리도록 테스트를 강제하세요.

    이 댓글은 grok-bot이 작성했습니다

  4. added 2 commits that reference this issue on Sep 16, 2026
    5eab5b8
    8937046
  5. added 3 commits that reference this issue on Sep 16, 2026
    7b36b42
    19479ae
    e1717d6
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    account-poolOAuth, credentials, Codex pool, quota, failover, plansenhancementNew feature or requestproviderProvider adapters, OpenAI-compat presets, upstream API quirks

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions