From c0556711285e1d07a01c80b2afd0e8d4f495f1a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=9A=B0=EC=B2=A0?= Date: Thu, 24 Sep 2026 09:44:35 +0900 Subject: [PATCH 1/3] fix(combo): cap explicit Retry-After cooldowns at one day --- docs-site/src/content/docs/guides/combos.md | 9 ++++---- .../src/content/docs/ko/guides/combos.md | 4 ++-- src/combos/failover.ts | 13 ++++++----- structure/runtime.md | 2 +- tests/codex-integration/combos.test.ts | 22 +++++++++++++++++++ 5 files changed, 37 insertions(+), 13 deletions(-) diff --git a/docs-site/src/content/docs/guides/combos.md b/docs-site/src/content/docs/guides/combos.md index 166ed292f56..adc37b2dd01 100644 --- a/docs-site/src/content/docs/guides/combos.md +++ b/docs-site/src/content/docs/guides/combos.md @@ -224,8 +224,9 @@ Combo failures are divided into **hop** failures and **terminal** failures. When `cooldownMs` is unset, a hopped target uses an upstream fallback: 5 seconds for request-rate 429s with upstream code `1302` or `1305`, and 60 seconds otherwise. When it is set, `cooldownMs` applies whenever no usable upstream `Retry-After` or Codex reset signal exists, including those -request-rate 429s. Numeric `Retry-After` seconds and HTTP-date values are accepted, and every -cooldown is capped at 10 minutes. The precedence is, from strongest to weakest, explicit +request-rate 429s. Numeric `Retry-After` seconds and HTTP-date values are accepted. Explicit +server delays are capped at 24 hours; reset-derived, configured, and fallback cooldowns are capped +at 10 minutes. The precedence is, from strongest to weakest, explicit `Retry-After` → Codex reset headers (`x-codex-primary-reset-at`, `x-codex-secondary-reset-at`, or `x-codex-tertiary-reset-at`) → the combo's `cooldownMs` (when set) → the 5-second request-rate fallback for upstream rate-limit codes `1302`/`1305` → the 60-second default. A valid immediate @@ -445,8 +446,8 @@ it has already been attempted for this request, or an encrypted v2 task excludes provider state and recent upstream errors. For cooldowns, follow an observed `Retry-After` value first; Codex reset headers also take precedence over `cooldownMs`. If neither upstream signal is usable, the configured `cooldownMs` applies, or the upstream fallback applies -when it is unset (5 seconds for request-rate codes `1302`/`1305`, otherwise 60 seconds); every cooldown is -capped at 10 minutes. +when it is unset (5 seconds for request-rate codes `1302`/`1305`, otherwise 60 seconds). Explicit +`Retry-After` delays are capped at 24 hours; the other cooldowns are capped at 10 minutes. ### Why was my alias rejected? diff --git a/docs-site/src/content/docs/ko/guides/combos.md b/docs-site/src/content/docs/ko/guides/combos.md index b49eb1ae35a..498cebbe150 100644 --- a/docs-site/src/content/docs/ko/guides/combos.md +++ b/docs-site/src/content/docs/ko/guides/combos.md @@ -135,7 +135,7 @@ ocx combo set balanced \ | 인프로세스 어댑터(`runTurn`)가 실행하는 Responses 턴에서 현재 요청이 선언하지 않은 첫 도구 호출(출력이나 재전송 불가 부작용 이전) | 대상을 쿨다운하고 같은 도구 카탈로그로 다음 대상으로 넘어갑니다. 출력이 보였거나 재전송 불가 부작용이 생긴 뒤에는 거부가 그대로 확정됩니다. Chat Completions와 Anthropic Messages 요청은 바뀌지 않습니다. | | 그 밖의 분류되지 않은 오류 | 멈추고 오류를 반환합니다. | -`cooldownMs`가 설정되지 않으면 홉된 대상은 업스트림 폴백을 사용합니다. 업스트림 코드 `1302` 또는 `1305`인 요청 속도 제한 429는 5초, 그 외에는 60초입니다. 설정하면 사용 가능한 업스트림 `Retry-After` 또는 Codex 재설정 신호가 없을 때, 해당 요청 속도 제한 429를 포함해 `cooldownMs`가 적용됩니다. 숫자로 된 `Retry-After` 초와 HTTP-date 값을 허용하며, 모든 쿨다운은 최대 10분으로 제한됩니다. 우선순위는 강한 순서대로 명시적 `Retry-After` → Codex 재설정 헤더(`x-codex-primary-reset-at`, `x-codex-secondary-reset-at`, 또는 `x-codex-tertiary-reset-at`) → 콤보의 `cooldownMs`(설정된 경우) → 업스트림 속도 제한 코드 `1302`/`1305`의 5초 요청 속도 제한 폴백 → 60초 기본값입니다. 유효한 즉시 지시인 `Retry-After: 0`은 설정된 쿨다운으로 대체하지 않고 업스트림의 즉시 지시로 유지합니다. +`cooldownMs`가 설정되지 않으면 홉된 대상은 업스트림 폴백을 사용합니다. 업스트림 코드 `1302` 또는 `1305`인 요청 속도 제한 429는 5초, 그 외에는 60초입니다. 설정하면 사용 가능한 업스트림 `Retry-After` 또는 Codex 재설정 신호가 없을 때, 해당 요청 속도 제한 429를 포함해 `cooldownMs`가 적용됩니다. 숫자로 된 `Retry-After` 초와 HTTP-date 값을 허용합니다. 명시적 서버 지연은 최대 24시간, 재설정 신호·설정값·폴백 쿨다운은 최대 10분으로 제한됩니다. 우선순위는 강한 순서대로 명시적 `Retry-After` → Codex 재설정 헤더(`x-codex-primary-reset-at`, `x-codex-secondary-reset-at`, 또는 `x-codex-tertiary-reset-at`) → 콤보의 `cooldownMs`(설정된 경우) → 업스트림 속도 제한 코드 `1302`/`1305`의 5초 요청 속도 제한 폴백 → 60초 기본값입니다. 유효한 즉시 지시인 `Retry-After: 0`은 설정된 쿨다운으로 대체하지 않고 업스트림의 즉시 지시로 유지합니다. 현재 요청은 이미 시도한 대상을 다시 시도하지 않습니다. 이후 요청은 쿨다운이 끝날 때까지 해당 대상을 건너뜁니다. 이미 지난 시각을 가리키는 `Retry-After` HTTP-date도 `Retry-After: 0`과 마찬가지로 업스트림의 즉시 지시로 유지됩니다. `waitForCooldownMs`를 설정하면 이후 요청은 가장 먼저 적합해지는 대상의 쿨다운을 선택 시도마다 이 한도까지 기다린 뒤 새로 한 번 선택합니다. 따라서 여러 failover 홉을 거치는 요청은 총 `hops × waitForCooldownMs`까지 기다릴 수 있습니다. 기본값은 `0`입니다. 모든 적합한 대상이 쿨다운 중이고 대기 한도가 0이거나 가장 이른 만료 시각이 대기 한도를 넘으면 요청은 즉시 HTTP 503으로 종료됩니다. 이 `combo_unavailable` 503에는 가장 이른 잔여 쿨다운과 같은 `Retry-After` 헤더가 포함되며, 값은 올림해 정수 초로 표시되고 최소 1초입니다. 대기에 지터를 적용하지 않으므로 동시에 깨어날 수 있습니다. 요청이 중단되면 이 대기가 취소되고 정상 `client_cancelled` 응답이 반환됩니다. 취소 후 백업 대상을 디스패치하지 않습니다. 콤보 대상 쿨다운은 프로세스 로컬 콤보별 상태입니다. 네이티브 계정 라우팅에서 사용하는 계정 수준 Codex 쿼터 쿨다운과는 별개입니다. @@ -262,7 +262,7 @@ opencodex 인스턴스에 기록했는지 확인하세요. 모든 대상이 현재 부적격 상태입니다. 예를 들어 프로바이더가 비활성화되었거나, cooldown 중이거나, 이 요청에서 이미 시도되었거나, 암호화된 v2 작업 때문에 제외되었을 수 있습니다. 대상 프로바이더 상태와 -최근 업스트림 오류를 확인하세요. 쿨다운에서는 먼저 응답의 `Retry-After` 값을 따르세요. Codex 재설정 헤더도 `cooldownMs`보다 우선합니다. 두 업스트림 신호를 모두 사용할 수 없을 때 설정된 `cooldownMs`를 적용하고, 미설정이면 업스트림 폴백(요청 속도 제한 코드 `1302`/`1305`는 5초, 그 외는 60초)을 적용하며 모든 쿨다운은 최대 10분으로 제한됩니다. +최근 업스트림 오류를 확인하세요. 쿨다운에서는 먼저 응답의 `Retry-After` 값을 따르세요. Codex 재설정 헤더도 `cooldownMs`보다 우선합니다. 두 업스트림 신호를 모두 사용할 수 없을 때 설정된 `cooldownMs`를 적용하고, 미설정이면 업스트림 폴백(요청 속도 제한 코드 `1302`/`1305`는 5초, 그 외는 60초)을 적용합니다. 명시적 `Retry-After` 지연은 최대 24시간, 나머지 쿨다운은 최대 10분입니다. ### alias가 거부된 이유는 무엇인가요? diff --git a/src/combos/failover.ts b/src/combos/failover.ts index 3b0434e244d..5e7ca9553f3 100644 --- a/src/combos/failover.ts +++ b/src/combos/failover.ts @@ -15,6 +15,7 @@ interface TargetCooldown { const DEFAULT_COOLDOWN_MS = 60_000; const MAX_COOLDOWN_MS = 10 * 60_000; +const MAX_SERVER_DELAY_MS = 24 * 60 * 60_000; /** Short cooldown for request-rate 429s (for example provider code 1302) that omit Retry-After. */ export const COMBO_REQUEST_RATE_COOLDOWN_MS = 5_000; @@ -115,6 +116,7 @@ function parseHttpDate(value: string, now: number): number | undefined { ); } +/** Parse a Retry-After delay, optionally retaining an upstream delay up to one day. */ export function parseRetryAfterMs( value: string | null | undefined, now = Date.now(), @@ -122,11 +124,10 @@ export function parseRetryAfterMs( ): number | undefined { const text = value?.trim(); if (!text) return undefined; - // A local wait ceiling must not make an explicit upstream reset expire early. - // Keep legacy bounded parsing for other callers. The opt-in stores a timestamp; - // the combo picker still independently limits how long a live request waits. + // Keep legacy bounded parsing for other callers. Combo cooldowns preserve + // multi-hour upstream delays, but never quarantine a target beyond one day. const maximum = options?.preserveServerDelay === true - ? Number.MAX_SAFE_INTEGER - Math.max(0, now) + ? MAX_SERVER_DELAY_MS : MAX_COOLDOWN_MS; if (/^\d+(?:\.\d+)?$/.test(text)) { const seconds = Number(text); @@ -200,6 +201,7 @@ export function comboCooldownRetryAfterSeconds(comboId: string, now = Date.now() return String(Math.max(1, Math.ceil(remainingMs / 1000))); } +/** Record a combo target cooldown, preferring bounded upstream retry evidence. */ export function coolComboTarget( comboId: string, target: Pick, @@ -234,8 +236,7 @@ export function coolComboTarget( message: options?.message, }) ? COMBO_REQUEST_RATE_COOLDOWN_MS : DEFAULT_COOLDOWN_MS); targetCooldowns.set(cooldownMapKey(comboId, target), { - // Only the locally chosen fallback is capped at ten minutes. An explicit - // server lower bound (including one hour) remains authoritative. + // Local fallbacks are capped at ten minutes; explicit server delays at one day. cooldownUntil: now + (serverDelayMs ?? Math.min(Math.max(cooldownMs, 1), MAX_COOLDOWN_MS)), }); sweepExpiredOnWrite(now); diff --git a/structure/runtime.md b/structure/runtime.md index 23ae1f37ea6..c8a18ed5426 100644 --- a/structure/runtime.md +++ b/structure/runtime.md @@ -496,7 +496,7 @@ adapter-local narrowing only and changes neither provider routing nor the serial A `response_format` capability refusal is a fifth envelope, kept separate because it needs one code and one frame the four above do not admit. The refusal must name `response_format` AND state that it is unavailable or unsupported; a message that merely names the field, such as an invalid-schema complaint, stays terminal, because replaying a malformed request at every later target is the outcome this distinction exists to avoid. `param` may be absent or explicitly null and a param naming another field fails closed. Its code set is the shared generic one plus `invalid_parameter_error`, held separately so the `user` and image branches are not widened by it. It also unwraps a single `data:` SSE frame on a one-line body — the reported gateway answers on the stream, so the error object is never extracted and the structured code arrives undefined — while a multi-event body is left alone. The next target receives the same request with `response_format` intact: no field is dropped and the output contract the caller asked for is unchanged. Traversal stays finite because combo excludes each attempted target. This verdict records no cooldown, and cancellation, origin/cyber-policy rejection and the non-replayable post-send codes are all tested before it (#4903). -The combo may advance to its next eligible unattempted target before output commitment. It records no target/provider cooldown for these request-local mismatches and does not silently drop reasoning controls or raise `none` to a supported rung. Cancellation, origin/cyber-policy rejection, non-replayable post-send errors and the existing streaming commit boundary stay authoritative. Apart from the definite context overflow below, other invalid requests remain terminal. +The combo may advance to its next eligible unattempted target before output commitment. It records no target/provider cooldown for these request-local mismatches and does not silently drop reasoning controls or raise `none` to a supported rung. Cancellation, origin/cyber-policy rejection, non-replayable post-send errors and the existing streaming commit boundary stay authoritative. Apart from the definite context overflow below, other invalid requests remain terminal. `src/combos/failover.ts` caps explicit upstream `Retry-After` target cooldowns at 24 hours while reset-derived, configured, and fallback cooldowns remain capped at 10 minutes. A definite context-window overflow is the fourth request-local verdict. A heterogeneous combo mixes windows, so "this turn does not fit THIS model" is not "this turn is impossible", and stopping at the first undersized target burned the ladder on turns a later target could hold. Evidence must come from the innermost provider message: `classifyError` remaps any occurrence of `context window`, `context length`, `maximum context` or `too many tokens` anywhere in the blob, and inheriting that looseness would let a `context_length_exceeded` token sitting in a `code` field beside `Unsupported parameter: user` authorize a replay. `src/combos/failover.ts` therefore unwraps only the exact proxy wrapper, within four envelopes and 16,384 characters, and reads the leaf message. A JSON-shaped body that does not parse fails closed, because `normalizeUpstreamErrorText` caps `classificationText` at 500 characters and a long envelope arrives here as a prefix. The verdict is admitted only for statuses that speak about the request — 400, 413, 422 and 5xx — so a 401/403 body that merely quotes context prose keeps its provider-wide cooldown instead of being rescored as request-shaped. Structured `origin_rejected`, cyber policy and the non-replayable post-send codes are all tested before it. diff --git a/tests/codex-integration/combos.test.ts b/tests/codex-integration/combos.test.ts index 00cbf4a28fd..6ecc00c1bf0 100644 --- a/tests/codex-integration/combos.test.ts +++ b/tests/codex-integration/combos.test.ts @@ -498,6 +498,28 @@ describe("combo target cooldowns", () => { expect(parseRetryAfterMs(new Date(now + 90_000).toUTCString(), now)).toBe(90_000); expect(parseRetryAfterMs(new Date(now + 90_000).toUTCString().toLowerCase(), now)).toBe(90_000); expect(parseRetryAfterMs(new Date(now + 900_000).toUTCString(), now)).toBe(600_000); + const serverDelay = { preserveServerDelay: true }; + expect(parseRetryAfterMs("14400", now, serverDelay)).toBe(14_400_000); + expect(parseRetryAfterMs("86400", now, serverDelay)).toBe(86_400_000); + expect(parseRetryAfterMs("999999", now, serverDelay)).toBe(86_400_000); + expect(parseRetryAfterMs(new Date(now + 4 * 60 * 60_000).toUTCString(), now, serverDelay)).toBe(14_400_000); + expect(parseRetryAfterMs(new Date(now + 2 * 86_400_000).toUTCString(), now, serverDelay)).toBe(86_400_000); + }); + + test("caps only explicit server cooldowns at one day", () => { + const now = Date.parse("2026-07-18T00:00:00.000Z"); + coolComboTarget("numeric-retry", target, { now, retryAfter: "999999" }); + coolComboTarget("date-retry", target, { now, retryAfter: new Date(now + 2 * 86_400_000).toUTCString() }); + coolComboTarget("multi-hour-retry", target, { now, retryAfter: "14400" }); + coolComboTarget("local-fallback", target, { now, cooldownMs: 999_999_999 }); + for (const comboId of ["numeric-retry", "date-retry"]) { + expect(isComboTargetInCooldown(comboId, target, now + 86_400_000 - 1)).toBe(true); + expect(isComboTargetInCooldown(comboId, target, now + 86_400_000)).toBe(false); + } + expect(isComboTargetInCooldown("multi-hour-retry", target, now + 14_400_000 - 1)).toBe(true); + expect(isComboTargetInCooldown("multi-hour-retry", target, now + 14_400_000)).toBe(false); + expect(isComboTargetInCooldown("local-fallback", target, now + 600_000 - 1)).toBe(true); + expect(isComboTargetInCooldown("local-fallback", target, now + 600_000)).toBe(false); }); test("rejects missing malformed zero and expired Retry-After values", () => { From 11a8c4d22c3c9fcd565ee1044d6348f2f22d6ac2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=9A=B0=EC=B2=A0?= Date: Thu, 24 Sep 2026 10:06:26 +0900 Subject: [PATCH 2/3] docs(combo): align translated cooldown ceilings --- docs-site/src/content/docs/fr/guides/combos.md | 4 ++-- docs-site/src/content/docs/ja/guides/combos.md | 4 ++-- docs-site/src/content/docs/ru/guides/combos.md | 4 ++-- docs-site/src/content/docs/tr/guides/combos.md | 4 ++-- docs-site/src/content/docs/zh-cn/guides/combos.md | 4 ++-- docs-site/src/content/docs/zh-tw/guides/combos.md | 4 ++-- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/docs-site/src/content/docs/fr/guides/combos.md b/docs-site/src/content/docs/fr/guides/combos.md index 9251951858b..09f96b182ac 100644 --- a/docs-site/src/content/docs/fr/guides/combos.md +++ b/docs-site/src/content/docs/fr/guides/combos.md @@ -207,7 +207,7 @@ Les échecs d’un combo se répartissent entre ceux qui entraînent un **bascul Une cible sautée entre en temps de recharge pendant 60 secondes par défaut. Si la réponse en amont inclut un valeur `Retry-After` valide, opencodex l’utilise à la place. Les secondes numériques et les valeurs de date HTTP sont -accepté, et chaque temps de recharge est limité à 10 minutes. +accepté, et un délai explicite `Retry-After` est plafonné à 24 heures ; les autres temps de recharge restent plafonnés à 10 minutes. La requête actuelle ne réessaye jamais la même cible tentée. Les demandes ultérieures l'ignorent jusqu'à ce qu'il soit le temps de recharge expire. S’il ne reste aucune cible éligible, le proxy renvoie HTTP 503 avec @@ -355,7 +355,7 @@ exécution d'une instance opencodex qui reçoit des requêtes de modèle. Chaque cible est actuellement inéligible : par exemple, son fournisseur est désactivé, il est en phase de refroidissement, elle a déjà été tentée pour cette requête, ou une tâche v2 chiffrée l'exclut. Vérifier la cible état du fournisseur et erreurs récentes en amont. Pour les temps de recharge, attendez la valeur par défaut de 60 secondes ou la -délai indiqué par `Retry-After` en amont (jamais plus de 10 minutes), puis réessayez. +délai indiqué par `Retry-After` en amont (au maximum 24 heures pour un `Retry-After` explicite, contre 10 minutes pour les autres), puis réessayez. ### Pourquoi mon alias a-t-il été rejeté ? diff --git a/docs-site/src/content/docs/ja/guides/combos.md b/docs-site/src/content/docs/ja/guides/combos.md index e77bba80380..4bb5110300e 100644 --- a/docs-site/src/content/docs/ja/guides/combos.md +++ b/docs-site/src/content/docs/ja/guides/combos.md @@ -129,7 +129,7 @@ ocx combo set balanced \ |インプロセスアダプター(`runTurn`)が実行する Responses ターンで、現在のリクエストが宣言していない最初のツール呼び出し(出力やリプレイ不可の副作用より前) |ターゲットをクールダウンし、同じツールカタログで次へ進みます。可視出力やリプレイ不可の副作用の後は拒否が確定します。Chat Completions と Anthropic Messages のリクエストは変わりません。 | |その他の未分類のエラー |停止してエラーを返します。 | -`cooldownMs` が未設定の場合、ホップされたターゲットはアップストリームのフォールバックを使用します。アップストリームコード `1302` または `1305` を伴うリクエストレート 429 では 5 秒、それ以外では 60 秒です。設定されている場合、使用可能なアップストリームの `Retry-After` または Codex リセットシグナルが存在しないときは、これらのリクエストレート 429 を含め、`cooldownMs` が適用されます。数値の `Retry-After` 秒数と HTTP-date 値が受け入れられ、すべてのクールダウンは 10 分を上限とします。優先順位は強い順に、明示的な `Retry-After` → Codex リセットヘッダー(`x-codex-primary-reset-at`、`x-codex-secondary-reset-at`、または `x-codex-tertiary-reset-at`)→ コンボの `cooldownMs`(設定時)→ アップストリームのレート制限コード `1302`/`1305` に対する 5 秒のリクエストレート フォールバック → 60 秒のデフォルトです。有効な即時指定 `Retry-After: 0` は、設定されたクールダウンで置き換えられず、即時のアップストリーム指示として維持されます。 +`cooldownMs` が未設定の場合、ホップされたターゲットはアップストリームのフォールバックを使用します。アップストリームコード `1302` または `1305` を伴うリクエストレート 429 では 5 秒、それ以外では 60 秒です。設定されている場合、使用可能なアップストリームの `Retry-After` または Codex リセットシグナルが存在しないときは、これらのリクエストレート 429 を含め、`cooldownMs` が適用されます。数値の `Retry-After` 秒数と HTTP-date 値が受け入れられ、明示的な上流の `Retry-After` は最大 24 時間、リセット由来・設定済み・フォールバックのクールダウンは最大 10 分です。優先順位は強い順に、明示的な `Retry-After` → Codex リセットヘッダー(`x-codex-primary-reset-at`、`x-codex-secondary-reset-at`、または `x-codex-tertiary-reset-at`)→ コンボの `cooldownMs`(設定時)→ アップストリームのレート制限コード `1302`/`1305` に対する 5 秒のリクエストレート フォールバック → 60 秒のデフォルトです。有効な即時指定 `Retry-After: 0` は、設定されたクールダウンで置き換えられず、即時のアップストリーム指示として維持されます。 現在のリクエストは、同じ試行ターゲットを再試行しません。後続のリクエストでは、クールダウンが期限切れになるまでそのターゲットをスキップします。すでに過去の時刻である `Retry-After` の HTTP-date も、`Retry-After: 0` と同様に即時のアップストリーム指示として維持されます。`waitForCooldownMs` を設定すると、後続のリクエストは、最も早く利用可能になるターゲットのクールダウンを、その選択試行ごとにこの上限まで待ってから、新たに 1 回選択できます。したがって、複数のフェイルオーバー ホップをまたぐリクエストは合計で `hops × waitForCooldownMs` まで待つ場合があります。デフォルトは `0` です。適格なターゲットがすべて冷却中の場合、HTTP 503 ですぐにフェイルクローズし、その `combo_unavailable` 503 には最も早く終了する残りのクールダウンと等しい `Retry-After` ヘッダーが含まれ、秒単位に切り上げられ、最小値は 1 秒です。待機にはジッターがないため、同期したウェイクアップが発生する可能性があります。中止されたリクエストはこの待機をキャンセルし、通常の `client_cancelled` 応答を返します。キャンセル後にバックアップ ターゲットをディスパッチすることはありません。コンボ ターゲットのクールダウンはプロセス ローカルなコンボごとの状態であり、ネイティブ アカウント ルーティングで使用されるアカウントレベルの Codex クォータ クールダウンとは別です。 @@ -254,7 +254,7 @@ ocx combo remove --yes ### `combo_unavailable` が発生するのはなぜですか? -現在、すべてのターゲットは不適格です。たとえば、プロバイダーが無効になっている、冷却中である、このリクエストに対してすでに試行されている、暗号化された v2 タスクによってターゲットが除外されているなどです。ターゲットプロバイダーの状態と最近のアップストリームエラーを確認してください。クールダウンでは、まずレスポンスで確認できる `Retry-After` の値に従ってください。Codex のリセットヘッダーも `cooldownMs` より優先され、どちらのアップストリームシグナルも使用できない場合は、設定した `cooldownMs`、未設定ならアップストリーム フォールバック(リクエストレートコード `1302`/`1305` では 5 秒、それ以外では 60 秒)が適用されますが、いずれのクールダウンも 10 分を超えません。 +現在、すべてのターゲットは不適格です。たとえば、プロバイダーが無効になっている、冷却中である、このリクエストに対してすでに試行されている、暗号化された v2 タスクによってターゲットが除外されているなどです。ターゲットプロバイダーの状態と最近のアップストリームエラーを確認してください。クールダウンでは、まずレスポンスで確認できる `Retry-After` の値に従ってください。Codex のリセットヘッダーも `cooldownMs` より優先され、どちらのアップストリームシグナルも使用できない場合は、設定した `cooldownMs`、未設定ならアップストリーム フォールバック(リクエストレートコード `1302`/`1305` では 5 秒、それ以外では 60 秒)が適用されますが、明示的な `Retry-After` は最大 24 時間、その他のクールダウンは最大 10 分です。 ### 私のエイリアスが拒否されたのはなぜですか? diff --git a/docs-site/src/content/docs/ru/guides/combos.md b/docs-site/src/content/docs/ru/guides/combos.md index db3e632e09d..d30cf3dfa9d 100644 --- a/docs-site/src/content/docs/ru/guides/combos.md +++ b/docs-site/src/content/docs/ru/guides/combos.md @@ -166,7 +166,7 @@ ocx combo set balanced \ | Первый вызов инструмента в Responses-ходе внутрипроцессного адаптера (`runTurn`), который текущий запрос не объявлял, до любого вывода и до побочного эффекта, небезопасного для повтора | Охлаждает цель и переходит к следующей с тем же каталогом инструментов. После видимого вывода или небезопасного для повтора побочного эффекта отказ окончателен. Запросы Chat Completions и Anthropic Messages не меняются. | | Любая другая неклассифицированная ошибка | Остановиться и вернуть ошибку. | -Если `cooldownMs` не задан, цель после hop использует upstream fallback: 5 секунд для 429, ограничивающих частоту запросов, с кодом upstream `1302` или `1305`, и 60 секунд в остальных случаях. Если он задан, `cooldownMs` применяется, когда нет пригодного сигнала upstream `Retry-After` или сигнала сброса Codex, включая такие 429, ограничивающие частоту запросов. Принимаются числовые секунды в `Retry-After` и значения HTTP-date; любой cooldown ограничен 10 минутами. Приоритет от сильного к слабому: явный `Retry-After` → заголовки сброса Codex (`x-codex-primary-reset-at`, `x-codex-secondary-reset-at` или `x-codex-tertiary-reset-at`) → `cooldownMs` этой combo (если задан) → 5-секундный fallback для rate-limit-кодов upstream `1302`/`1305` → стандартные 60 секунд. Корректный немедленный `Retry-After: 0` сохраняется как немедленная директива upstream, а не заменяется настроенным cooldown. +Если `cooldownMs` не задан, цель после hop использует upstream fallback: 5 секунд для 429, ограничивающих частоту запросов, с кодом upstream `1302` или `1305`, и 60 секунд в остальных случаях. Если он задан, `cooldownMs` применяется, когда нет пригодного сигнала upstream `Retry-After` или сигнала сброса Codex, включая такие 429, ограничивающие частоту запросов. Принимаются числовые секунды в `Retry-After` и значения HTTP-date; явный `Retry-After` ограничен 24 часами, а cooldown по сбросу, настройке или fallback — 10 минутами. Приоритет от сильного к слабому: явный `Retry-After` → заголовки сброса Codex (`x-codex-primary-reset-at`, `x-codex-secondary-reset-at` или `x-codex-tertiary-reset-at`) → `cooldownMs` этой combo (если задан) → 5-секундный fallback для rate-limit-кодов upstream `1302`/`1305` → стандартные 60 секунд. Корректный немедленный `Retry-After: 0` сохраняется как немедленная директива upstream, а не заменяется настроенным cooldown. Текущий запрос никогда не повторяет уже опробованную цель. Более поздние запросы пропускают её, пока не истечёт cooldown. HTTP-date в `Retry-After`, указывающий на уже прошедшее время, также сохраняется как немедленная директива upstream, как и `Retry-After: 0`. Задайте `waitForCooldownMs`, чтобы следующий запрос мог подождать cooldown цели, которая станет подходящей раньше всех, до этого ограничения при каждой попытке выбора, а затем выполнить один новый выбор. Поэтому запрос с несколькими hop в failover может ждать в общей сложности до `hops × waitForCooldownMs`. По умолчанию это `0`: если все подходящие цели находятся в cooldown, запрос немедленно завершается HTTP 503; этот ответ `combo_unavailable` содержит заголовок `Retry-After`, равный оставшемуся cooldown цели с самым ранним окончанием, округлённый вверх до целых секунд, минимум до 1 секунды. К ожиданиям не добавляется джиттер, поэтому возможны синхронные пробуждения. Отмена запроса отменяет это ожидание и возвращает обычный ответ `client_cancelled`; после отмены резервная цель не запускается. Cooldown цели combo — состояние процесса, отдельное для каждой combo; он не связан с cooldown квоты Codex на уровне аккаунта, который используется нативной маршрутизацией аккаунта. @@ -311,7 +311,7 @@ Id combo неизвестен. Ответ — HTTP 404 с типом `invalid_re Сейчас ни одна цель не подходит: например, провайдер отключён, находится в cooldown, уже был испробован для этого запроса или исключён из-за шифрованной задачи v2. Проверьте состояние -провайдеров цели и недавние upstream-ошибки. Для cooldown сначала следуйте значению `Retry-After` из ответа. Заголовки сброса Codex также имеют приоритет над `cooldownMs`, поэтому если ни один upstream-сигнал не пригоден, применяется заданный `cooldownMs`, а если он не задан — upstream fallback (5 секунд для rate-limit-кодов `1302`/`1305`, иначе 60 секунд); любой cooldown ограничен 10 минутами. +провайдеров цели и недавние upstream-ошибки. Для cooldown сначала следуйте значению `Retry-After` из ответа. Заголовки сброса Codex также имеют приоритет над `cooldownMs`, поэтому если ни один upstream-сигнал не пригоден, применяется заданный `cooldownMs`, а если он не задан — upstream fallback (5 секунд для rate-limit-кодов `1302`/`1305`, иначе 60 секунд); явный `Retry-After` ограничен 24 часами, а cooldown по сбросу, настройке или fallback — 10 минутами. ### Почему alias был отклонён? diff --git a/docs-site/src/content/docs/tr/guides/combos.md b/docs-site/src/content/docs/tr/guides/combos.md index a837a74e312..c8d257c31c6 100644 --- a/docs-site/src/content/docs/tr/guides/combos.md +++ b/docs-site/src/content/docs/tr/guides/combos.md @@ -237,7 +237,7 @@ ikiye ayrılır. Atlanan bir hedef varsayılan olarak 60 saniye boyunca soğuma süresine girer. Yukarı akış yanıtı geçerli bir `Retry-After` değeri içeriyorsa opencodex bunun yerine onu kullanır. Sayısal saniyeler ve HTTP tarihi değerleri kabul edilir ve -her soğuma süresi en fazla 10 dakika ile sınırlandırılır. +açık `Retry-After` gecikmesi en fazla 24 saat, sıfırlama kaynaklı, yapılandırılmış ve varsayılan soğuma süreleri en fazla 10 dakika ile sınırlandırılır. Geçerli istek denenen aynı hedefi asla yeniden denemez. Daha sonraki istekler soğuma süresi dolana kadar onu atlar. Uygun hiçbir hedef kalmazsa proxy @@ -390,7 +390,7 @@ Her hedef şu anda uygun değildir: örneğin sağlayıcısı devre dışıdır, soğumaktadır, bu istek için zaten denenmiştir veya şifrelenmiş bir v2 görevi onu hariç tutmaktadır. Hedef sağlayıcı durumunu ve son yukarı akış hatalarını kontrol edin. Soğuma süreleri için 60 saniyelik varsayılanı veya yukarı akış -`Retry-After` süresini (asla 10 dakikadan fazla olamaz) bekleyin, ardından +`Retry-After` süresini (açık `Retry-After` için en fazla 24 saat, diğer soğuma süreleri için en fazla 10 dakika) bekleyin, ardından yeniden deneyin. ### Takma adım neden reddedildi? diff --git a/docs-site/src/content/docs/zh-cn/guides/combos.md b/docs-site/src/content/docs/zh-cn/guides/combos.md index dae57bd773f..7fca4cc0770 100644 --- a/docs-site/src/content/docs/zh-cn/guides/combos.md +++ b/docs-site/src/content/docs/zh-cn/guides/combos.md @@ -155,7 +155,7 @@ combo 失败分为 **跳转** 失败和 **终止** 失败。 | 由进程内适配器(`runTurn`)执行的 Responses 回合中,当前请求未声明的第一个工具调用(在任何输出和不可重放的副作用之前) | 让该目标进入冷却,并以相同的工具目录跳转到下一个目标。出现可见输出或不可重放的副作用之后,拒绝即为最终结果。Chat Completions 和 Anthropic Messages 请求不受影响。 | | 任何其他未分类错误 | 停止并返回错误。 | -未设置 `cooldownMs` 时,发生跳转的目标使用上游回退值:对于上游代码为 `1302` 或 `1305` 的请求速率限制 429,等待 5 秒;其他情况等待 60 秒。设置后,只要不存在可用的上游 `Retry-After` 或 Codex 重置信号,就会应用 `cooldownMs`,包括这些请求速率限制 429。接受数字形式的 `Retry-After` 秒数和 HTTP-date 值,每次冷却最多封顶 10 分钟。优先级从强到弱依次为:显式 `Retry-After` → Codex 重置标头(`x-codex-primary-reset-at`、`x-codex-secondary-reset-at` 或 `x-codex-tertiary-reset-at`)→ combo 的 `cooldownMs`(已设置时)→ 上游速率限制代码 `1302`/`1305` 的 5 秒请求速率限制回退值 → 60 秒默认值。有效的即时指令 `Retry-After: 0` 会保留为上游即时指令,不会被配置的冷却替换。 +未设置 `cooldownMs` 时,发生跳转的目标使用上游回退值:对于上游代码为 `1302` 或 `1305` 的请求速率限制 429,等待 5 秒;其他情况等待 60 秒。设置后,只要不存在可用的上游 `Retry-After` 或 Codex 重置信号,就会应用 `cooldownMs`,包括这些请求速率限制 429。接受数字形式的 `Retry-After` 秒数和 HTTP-date 值,显式上游 `Retry-After` 最多 24 小时;重置推导、配置和回退冷却最多 10 分钟。优先级从强到弱依次为:显式 `Retry-After` → Codex 重置标头(`x-codex-primary-reset-at`、`x-codex-secondary-reset-at` 或 `x-codex-tertiary-reset-at`)→ combo 的 `cooldownMs`(已设置时)→ 上游速率限制代码 `1302`/`1305` 的 5 秒请求速率限制回退值 → 60 秒默认值。有效的即时指令 `Retry-After: 0` 会保留为上游即时指令,不会被配置的冷却替换。 当前请求不会再次重试同一个已经尝试过的目标。后续请求会跳过它,直到冷却结束。已过去的 HTTP-date `Retry-After` 同样会像 `Retry-After: 0` 一样保留为上游即时指令。设置 `waitForCooldownMs` 后,后续请求可以等待最早恢复资格的目标的冷却,单次选择尝试最多等待该上限,然后重新选择一次。因此,多次故障切换跳转的请求总共最多等待 `hops × waitForCooldownMs`。默认值为 `0`;当所有合格目标都处于冷却中时,请求会立即失败并返回 HTTP 503;该 `combo_unavailable` 503 会带有 `Retry-After` 标头,其值等于剩余冷却时间最短的目标,向上取整为整秒,最小值为 1 秒。等待不加入抖动,因此可能同时唤醒。请求中止会取消这次等待并返回正常的 `client_cancelled` 响应;取消后不会调度备用目标。combo 目标冷却是进程本地、按 combo 区分的状态,与原生账户路由使用的账户级 Codex 配额冷却彼此独立。 @@ -284,7 +284,7 @@ combo id 不存在。响应是 HTTP 404,类型为 `invalid_request_error`。 ### 为什么会收到 `combo_unavailable`? -当前每个目标都不可用:例如,它的 provider 被禁用、它正在冷却、它已经在这次请求中被尝试过,或者加密的 v2 任务把它排除了。检查目标的 provider 状态和最近的上游错误。对于冷却,请先遵循响应中的 `Retry-After` 值。Codex 重置标头的优先级也高于 `cooldownMs`;只有在两个上游信号都不可用时,才应用已配置的 `cooldownMs`,未配置时应用上游回退值(请求速率限制代码 `1302`/`1305` 为 5 秒,否则为 60 秒),且所有冷却最多封顶 10 分钟。 +当前每个目标都不可用:例如,它的 provider 被禁用、它正在冷却、它已经在这次请求中被尝试过,或者加密的 v2 任务把它排除了。检查目标的 provider 状态和最近的上游错误。对于冷却,请先遵循响应中的 `Retry-After` 值。Codex 重置标头的优先级也高于 `cooldownMs`;只有在两个上游信号都不可用时,才应用已配置的 `cooldownMs`,未配置时应用上游回退值(请求速率限制代码 `1302`/`1305` 为 5 秒,否则为 60 秒),显式 `Retry-After` 最多 24 小时,其他冷却最多 10 分钟。 ### 为什么我的别名被拒绝了? diff --git a/docs-site/src/content/docs/zh-tw/guides/combos.md b/docs-site/src/content/docs/zh-tw/guides/combos.md index d41d01736be..0ece36f42a1 100644 --- a/docs-site/src/content/docs/zh-tw/guides/combos.md +++ b/docs-site/src/content/docs/zh-tw/guides/combos.md @@ -169,7 +169,7 @@ Combo 失敗分為**跳轉**失敗與**終端**失敗。 | 由行程內轉接器(`runTurn`)執行的 Responses 回合中,目前請求未宣告的第一個工具呼叫(在任何輸出與不可重播的副作用之前) | 讓該目標進入冷卻,並以相同的工具目錄跳轉到下一個目標。出現可見輸出或不可重播的副作用之後,拒絕即為最終結果。Chat Completions 與 Anthropic Messages 請求不受影響。 | | 任何其他未分類錯誤 | 停止並回傳錯誤。 | -跳轉的目標預設進入 60 秒冷卻。若上游回應包含有效的 `Retry-After` 值,opencodex 改用它。接受數字秒與 HTTP-date 值,且每次冷卻上限為 10 分鐘。 +跳轉的目標預設進入 60 秒冷卻。若上游回應包含有效的 `Retry-After` 值,opencodex 改用它。接受數字秒與 HTTP-date 值。明確的上游 `Retry-After` 最長為 24 小時;重設推導、設定與預設冷卻最長為 10 分鐘。 目前請求永不重試同一已嘗試目標。後續請求會略過它直到冷卻到期。若無合格目標剩餘,代理回傳 HTTP 503 並帶 `error.code = "combo_unavailable"`。 @@ -284,7 +284,7 @@ Combo id 未知。回應為 HTTP 404 並帶 type `invalid_request_error`。執 ### 為什麼我得到 `combo_unavailable`? -每個目標目前都不合格:例如其供應商已停用、冷卻中、已為此請求嘗試過,或加密 v2 任務排除它。檢查目標供應商狀態與近期上游錯誤。對於冷卻,等待 60 秒預設或上游 `Retry-After` 期間(絕不超過 10 分鐘),然後重試。 +每個目標目前都不合格:例如其供應商已停用、冷卻中、已為此請求嘗試過,或加密 v2 任務排除它。檢查目標供應商狀態與近期上游錯誤。對於冷卻,等待 60 秒預設或上游 `Retry-After` 期間(明確的上游 `Retry-After` 最長 24 小時,其他冷卻最長 10 分鐘),然後重試。 ### 為什麼我的別名被拒絕? From f70015c09c73afbd751e306cabb18f6f87e9de97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=9A=B0=EC=B2=A0?= Date: Thu, 24 Sep 2026 10:35:07 +0900 Subject: [PATCH 3/3] docs(combo): clarify cooldown contract and regression intent --- structure/runtime.md | 8 ++++---- tests/codex-integration/combos.test.ts | 2 ++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/structure/runtime.md b/structure/runtime.md index c8a18ed5426..0fd4cdb148e 100644 --- a/structure/runtime.md +++ b/structure/runtime.md @@ -486,9 +486,7 @@ Renamed fixed-key providers receive [missing reasoning metadata](catalog.md#rena Translated audio/file admission follows the [final-adapter input contract](adapters/registry.md#untranslated-input-media); native raw passthrough remains separate. ## Request-local target compatibility -Google's final adapter compiler may emit an opt-in, content-free -[tool-schema loss diagnostic](providers/google.md#google-tool-schema-loss-reporting). It observes -adapter-local narrowing only and changes neither provider routing nor the serialized request body. +Google's final adapter compiler may emit an opt-in, content-free [tool-schema loss diagnostic](providers/google.md#google-tool-schema-loss-reporting). It observes adapter-local narrowing only and changes neither provider routing nor the serialized request body. `src/adapters/openai-responses.ts` omits only top-level `user` at the canonical ChatGPT Codex forward destination. Claude translation retains its original identity and prompt-cache key; public API and noncanonical gateways retain their `user` field. Input roles, tool-schema properties, safety identifiers and original replay bodies are not changed. @@ -496,7 +494,7 @@ adapter-local narrowing only and changes neither provider routing nor the serial A `response_format` capability refusal is a fifth envelope, kept separate because it needs one code and one frame the four above do not admit. The refusal must name `response_format` AND state that it is unavailable or unsupported; a message that merely names the field, such as an invalid-schema complaint, stays terminal, because replaying a malformed request at every later target is the outcome this distinction exists to avoid. `param` may be absent or explicitly null and a param naming another field fails closed. Its code set is the shared generic one plus `invalid_parameter_error`, held separately so the `user` and image branches are not widened by it. It also unwraps a single `data:` SSE frame on a one-line body — the reported gateway answers on the stream, so the error object is never extracted and the structured code arrives undefined — while a multi-event body is left alone. The next target receives the same request with `response_format` intact: no field is dropped and the output contract the caller asked for is unchanged. Traversal stays finite because combo excludes each attempted target. This verdict records no cooldown, and cancellation, origin/cyber-policy rejection and the non-replayable post-send codes are all tested before it (#4903). -The combo may advance to its next eligible unattempted target before output commitment. It records no target/provider cooldown for these request-local mismatches and does not silently drop reasoning controls or raise `none` to a supported rung. Cancellation, origin/cyber-policy rejection, non-replayable post-send errors and the existing streaming commit boundary stay authoritative. Apart from the definite context overflow below, other invalid requests remain terminal. `src/combos/failover.ts` caps explicit upstream `Retry-After` target cooldowns at 24 hours while reset-derived, configured, and fallback cooldowns remain capped at 10 minutes. +The combo may advance to its next eligible unattempted target before output commitment. It records no target/provider cooldown for these request-local mismatches and does not silently drop reasoning controls or raise `none` to a supported rung. Cancellation, origin/cyber-policy rejection, non-replayable post-send errors and the existing streaming commit boundary stay authoritative. Apart from the definite context overflow below, other invalid requests remain terminal. A definite context-window overflow is the fourth request-local verdict. A heterogeneous combo mixes windows, so "this turn does not fit THIS model" is not "this turn is impossible", and stopping at the first undersized target burned the ladder on turns a later target could hold. Evidence must come from the innermost provider message: `classifyError` remaps any occurrence of `context window`, `context length`, `maximum context` or `too many tokens` anywhere in the blob, and inheriting that looseness would let a `context_length_exceeded` token sitting in a `code` field beside `Unsupported parameter: user` authorize a replay. `src/combos/failover.ts` therefore unwraps only the exact proxy wrapper, within four envelopes and 16,384 characters, and reads the leaf message. A JSON-shaped body that does not parse fails closed, because `normalizeUpstreamErrorText` caps `classificationText` at 500 characters and a long envelope arrives here as a prefix. The verdict is admitted only for statuses that speak about the request — 400, 413, 422 and 5xx — so a 401/403 body that merely quotes context prose keeps its provider-wide cooldown instead of being rescored as request-shaped. Structured `origin_rejected`, cyber policy and the non-replayable post-send codes are all tested before it. @@ -504,6 +502,8 @@ This is also why the classifier cannot duplicate visible output. Native byte str Regression coverage: `tests/responses/responses-forward-prompt-envelope.test.ts`, `tests/routing/router-combo-failover-classification.test.ts`, `tests/routing/routing-policy-fallback.test.ts`, `tests/helpers/combo-context-overflow-cases.ts`, and `tests/server/server-combo-failover-e2e.test.ts`. +`src/combos/failover.ts` caps explicit upstream `Retry-After` target cooldowns at 24 hours while reset-derived, configured, and fallback cooldowns remain capped at 10 minutes. + ## Combo default effort precedence `src/combos/request.ts` keeps `reasoningEffortMode` and `defaultEffortMode` independent. diff --git a/tests/codex-integration/combos.test.ts b/tests/codex-integration/combos.test.ts index 6ecc00c1bf0..3b8472de6af 100644 --- a/tests/codex-integration/combos.test.ts +++ b/tests/codex-integration/combos.test.ts @@ -490,6 +490,7 @@ describe("combo request cloning", () => { describe("combo target cooldowns", () => { const target = { provider: "a", model: "m1" }; + /** Verify numeric and HTTP-date delays obey the selected local or server ceiling. */ test("parses numeric and date Retry-After values with exact bounds", () => { const now = Date.parse("2026-07-18T00:00:00.000Z"); expect(parseRetryAfterMs("0.001", now)).toBe(1); @@ -506,6 +507,7 @@ describe("combo target cooldowns", () => { expect(parseRetryAfterMs(new Date(now + 2 * 86_400_000).toUTCString(), now, serverDelay)).toBe(86_400_000); }); + /** Verify recorded cooldowns expire at their own ceiling without truncating valid delays. */ test("caps only explicit server cooldowns at one day", () => { const now = Date.parse("2026-07-18T00:00:00.000Z"); coolComboTarget("numeric-retry", target, { now, retryAfter: "999999" });