From 09acdca0ce9b83a35709a9ea671a6eef2f484980 Mon Sep 17 00:00:00 2001 From: JUN Date: Wed, 11 Nov 2026 03:03:23 +0900 Subject: [PATCH 1/2] fix(responses): let retryOnReset replace chained native turns A Codex turn chained with previous_response_id is expanded from the proxy's own replay state, and the native Responses adapter strips the id before every send. The ambiguous-reset judgment still read the inbound body, which keeps the id, so every chained turn was refused and an operator's retryOnReset opt-in never applied to it. A pre-header reset answered the synthetic 429 upstream_reset_replay_refused after one send. Judge the native passthrough body with a locally expanded previous_response_id removed; an unexpanded id still reaches the wire and still refuses. Log the gate's refusal reason (ambiguous-no-policy, ambiguous-request-not-replayable, ambiguous-allowance-spent) with host and stage only, so an operator can tell why an opt-in did not apply. Move responses-reset-replay.test.ts into the responses layout shard so layout.json drops back under the 2000-line ratchet threshold. Closes #8453 --- .../fr/reference/configuration/providers.md | 2 +- .../ja/reference/configuration/providers.md | 2 +- .../ko/reference/configuration/providers.md | 2 +- .../docs/reference/configuration/providers.md | 2 +- .../ru/reference/configuration/providers.md | 2 +- .../tr/reference/configuration/providers.md | 2 +- .../reference/configuration/providers.md | 2 +- .../reference/configuration/providers.md | 2 +- scripts/test-layout/layout-responses.json | 4 +- scripts/test-layout/layout.json | 1 - src/server/responses/passthrough-dispatch.ts | 13 ++- src/server/responses/reset-replay.ts | 60 ++++++++++- structure/transports/responses-failover.md | 1 + tests/fixtures/test-layout-expected.json | 2 +- .../responses-reset-replay-chained.test.ts | 101 ++++++++++++++++++ 15 files changed, 178 insertions(+), 20 deletions(-) create mode 100644 tests/responses/responses-reset-replay-chained.test.ts diff --git a/docs-site/src/content/docs/fr/reference/configuration/providers.md b/docs-site/src/content/docs/fr/reference/configuration/providers.md index c4c8241454d..7938f369480 100644 --- a/docs-site/src/content/docs/fr/reference/configuration/providers.md +++ b/docs-site/src/content/docs/fr/reference/configuration/providers.md @@ -173,7 +173,7 @@ sauvegarde dont le contenu diffère, puis réécrit en identifiants sans préfix | `promptCacheKey?` | `boolean` | Active pour tout le fournisseur `openai-chat` la transmission de `prompt_cache_key`. L’adaptateur transmet la clé reçue sans en inventer ; la traduction Claude Messages peut toutefois la dériver de `metadata.user_id`, ou d’un groupe modèle/système/outils sans métadonnées. Désactivé par défaut sauf dans un préréglage ; Mistral l’active selon son [contrat de cache](https://docs.mistral.ai/studio/conversations/advanced/prompt-caching). `false` explicite le désactive. N’activer que si l’amont documente ce champ, car une passerelle stricte peut répondre HTTP 400. | | `retryOn429?` | `{ enabled?: boolean; attempts?: number; intervalMs?: number; maxIntervalMs?: number; respectRetryAfter?: boolean }` | Réservé aux fournisseurs à clé API (`authMode: "key"`). Facultatif : absent, désactivé, sauf pour OpenCode Go et Command Code sur leurs destinations canoniques et Mistral canonique avec `openai-chat` : 6 relances, intervalle de 10 s, plafond de 60 s. Un objet active la fonction ; `enabled: false` désactive aussi ces valeurs de repli. Après un HTTP 429 avant le flux, attend `Retry-After` ou l’intervalle fixe, puis renvoie la même requête avec la même clé avant le basculement. Couvre la récupération du tour texte, Responses passthrough, les ponts image/vidéo, le service web-search et les continuations terminales ; les transports `runTurn` personnalisés sont exclus. `attempts` compte les relances après le premier 429 (envois totaux = `attempts` + 1), dans un budget partagé à l’échelle de la requête. Son épuisement arrête les relances avec la même clé, puis laisse agir le basculement ou l’erreur finale ; le passthrough à clé n’a pas de basculement. Pour Mistral Chat canonique, un délai explicite supérieur à l’attente autorisée arrête la relance avec la même clé et conserve le `Retry-After` original ; une autre clé du pool peut encore servir. Les autres fournisseurs conservent le délai plafonné. Mistral partage les échéances par identifiant/modèle, sauf avec `requestPacing.enabled: false` ou `retryOn429.respectRetryAfter: false`. Un 429 sans échéance avec une pression validée sur le budget par minute déclenche une attente prudente de 60 s, puis une seule sonde ; son succès rétablit le trafic ordinaire. Le trafic sain ne prédit pas le coût suivant. La politique utilise les allocations annoncées, exclut les allocations mensuelles connues comme épuisées, ne prédit pas une réinitialisation exacte et ne déclenche pas de compactage. Codex ne relance pas lui-même les 429. Valeurs par défaut de l’objet : `enabled: true`, `attempts: 3`, `intervalMs: 5000`, `maxIntervalMs: 60000` (attente plafonnée ; valeur maximale 600000), `respectRetryAfter: true`. | | `transientRetryOn5xx?` | `{ enabled?: boolean; attempts?: number }` | Fournisseurs `openai-chat` et `openai-responses` authentifiés par clé uniquement. Les fournisseurs `authMode: "forward"` (le pool de comptes ChatGPT) ne lisent jamais cette option et conservent l'échelle par défaut. Nouvelle tentative facultative pour les états transitoires reçus en amont avant le début de la diffusion (500, 502, 503, 504, 520, 521, 522) : l'absence de l'option la désactive ; la présence d'un objet l'active, sauf avec `enabled: false`. Ce comportement couvre la requête Responses initiale, la continuation de la garde du terminal, le point de terminaison natif `/v1/chat/completions` et les réémissions liées à la récupération après un 429 ou à la récupération de compte. `attempts` représente le nombre TOTAL d'envois en amont autorisés pour une requête, premier envoi compris (de 1 à 10, valeur par défaut : 3). Il constitue un budget commun à la requête, partagé avec la récupération après une réinitialisation de connexion ; ainsi, `3` signifie qu'au plus trois requêtes réelles atteignent le fournisseur. Les attentes utilisent une temporisation exponentielle à base fixe de 400 ms, plafonnée à 5 s, et respectent `Retry-After`. Cette option est distincte de `retryOn429`, qui traite la limitation de débit ; les échecs en cours de diffusion ne sont jamais relus. Chaque cible Combo compte son premier envoi prépayé dans son propre total configuré : `attempts: 1` autorise un envoi, sans rembourser la réservation ni élargir le plafond partagé. Un refus local ou une annulation avant l'envoi libère la réservation inutilisée. | -| `retryOnReset?` | `{ enabled?: boolean; replacements?: number }` | Fournisseurs `openai-responses` natifs, `authMode: "forward"` compris, ainsi que les requêtes Responses traduites envoyées par la voie générique avant les en-têtes reçus en amont. Remplacement facultatif d'un envoi qui a échoué alors que l'appelant n'avait rien observé : l'absence de l'option la désactive ; la présence d'un objet l'active, sauf avec `enabled: false`. Couvre les deux étapes ambiguës — une connexion rompue avant tout en-tête de réponse, et un corps SSE rompu après l'en-tête alors qu'il ne portait que des événements de contrôle. Une WebSocket canonique ChatGPT en amont qui s'est fermée ou a échoué après l'envoi de sa trame de création, avant tout événement Responses, est couverte de la même façon ; son remplacement est envoyé en HTTP. Seule une requête autonome est remplacée : `store: false`, `input` complet, ni `previous_response_id`, ni `conversation`, ni `stream_id`, et uniquement des outils exécutés par le client. `replacements` est le nombre d'envois de remplacement qu'UNE requête logique peut effectuer, toutes étapes et tous enfants de combo confondus (de 1 à 2, valeur par défaut : 1). Ce n'est ni un nombre de tentatives par étape ni un budget d'envoi : un remplacement doit toujours tenir dans l'allocation d'envois dont l'étape disposait déjà. Une requête qui a déjà émis une sortie ou un appel d'outil n'est jamais remplacée, quelle que soit cette valeur. L'inférence de remplacement peut tout de même être facturée si l'origine avait déjà démarré la première, d'où la désactivation par défaut. La voie générique de traduction prend en charge le remplacement après une rupture avant les en-têtes pour les envois initiaux et reconstruits, avec la même autorisation et le même budget d’envois. Les transports gérés par les adaptateurs et les erreurs des flux traduits après les en-têtes reçus en amont sont exclus. | +| `retryOnReset?` | `{ enabled?: boolean; replacements?: number }` | Fournisseurs `openai-responses` natifs, `authMode: "forward"` compris, ainsi que les requêtes Responses traduites envoyées par la voie générique avant les en-têtes reçus en amont. Remplacement facultatif d'un envoi qui a échoué alors que l'appelant n'avait rien observé : l'absence de l'option la désactive ; la présence d'un objet l'active, sauf avec `enabled: false`. Couvre les deux étapes ambiguës — une connexion rompue avant tout en-tête de réponse, et un corps SSE rompu après l'en-tête alors qu'il ne portait que des événements de contrôle. Une WebSocket canonique ChatGPT en amont qui s'est fermée ou a échoué après l'envoi de sa trame de création, avant tout événement Responses, est couverte de la même façon ; son remplacement est envoyé en HTTP. Seule une requête autonome est remplacée : `store: false`, `input` complet, ni `previous_response_id`, ni `conversation`, ni `stream_id`, et uniquement des outils exécutés par le client. Un tour chaîné dont ce proxy a déjà développé le `previous_response_id` à partir de son propre état de rejeu est admissible, car cet identifiant est retiré avant l'envoi. Quand une réinitialisation n'est pas remplacée, le journal du service en indique la raison : `ambiguous-no-policy`, `ambiguous-request-not-replayable` ou `ambiguous-allowance-spent`. `replacements` est le nombre d'envois de remplacement qu'UNE requête logique peut effectuer, toutes étapes et tous enfants de combo confondus (de 1 à 2, valeur par défaut : 1). Ce n'est ni un nombre de tentatives par étape ni un budget d'envoi : un remplacement doit toujours tenir dans l'allocation d'envois dont l'étape disposait déjà. Une requête qui a déjà émis une sortie ou un appel d'outil n'est jamais remplacée, quelle que soit cette valeur. L'inférence de remplacement peut tout de même être facturée si l'origine avait déjà démarré la première, d'où la désactivation par défaut. La voie générique de traduction prend en charge le remplacement après une rupture avant les en-têtes pour les envois initiaux et reconstruits, avec la même autorisation et le même budget d’envois. Les transports gérés par les adaptateurs et les erreurs des flux traduits après les en-têtes reçus en amont sont exclus. | | `autoToolChoiceOnlyModels?` | `string[]` | Modèles dont `tool_choice` accepte uniquement `auto` ou `none` ; les choix forcés sont dévalorisés. | | `preserveReasoningContentModels?` | `string[]` | Modèles nécessitant un assistant préalable `reasoning_content` dans l'historique des discussions. Les enregistrements depuis le tableau de bord conservent la liste enregistrée, y compris `[]`. `PATCH /api/providers?name=` accepte un tableau ou `null` pour l'effacer. Une sauvegarde qui déplace le fournisseur vers un autre adaptateur, une autre URL de base ou un autre mode d'authentification ne la conserve pas (voir la section ci-dessous). | | `reasoningDetailsModels?` | `string[]` | Modèles dont le point de terminaison renvoie la réflexion sous forme de tableau structuré `reasoning_details` (MiniMax série M avec `reasoning_split`) ; les deltas de flux sont des instantanés cumulatifs comparés par préfixe, et la réflexion conservée est rejouée sous forme de tableau `reasoning_details` plutôt que de chaîne `reasoning_content`. | diff --git a/docs-site/src/content/docs/ja/reference/configuration/providers.md b/docs-site/src/content/docs/ja/reference/configuration/providers.md index a138683925a..6a5359d6f76 100644 --- a/docs-site/src/content/docs/ja/reference/configuration/providers.md +++ b/docs-site/src/content/docs/ja/reference/configuration/providers.md @@ -165,7 +165,7 @@ account を削除しても mapping は保持され、同じ id を再追加す | `promptCacheKey?` | `boolean` | `openai-chat` のプロバイダー全体で `prompt_cache_key` の転送を有効にします。アダプターは受け取ったキーを転送し生成しませんが、Claude Messages 変換では `metadata.user_id`、またはメタデータがなければモデル・システム・ツールの組み合わせから導出します。プリセットが指定しない限り既定は無効です。Mistral は[プロンプトキャッシュ契約](https://docs.mistral.ai/studio/conversations/advanced/prompt-caching)に従い有効で、明示的な `false` で無効になります。上流が対応を明記する場合のみ有効にしてください。厳格なゲートウェイは未知のフィールドを HTTP 400 で拒否する場合があります。 | | `retryOn429?` | `{ enabled?: boolean; attempts?: number; intervalMs?: number; maxIntervalMs?: number; respectRetryAfter?: boolean }` | API キーのプロバイダーのみ(`authMode: "key"`)。未設定なら無効ですが、正規の OpenCode Go と Command Code、および正規の Mistral の `openai-chat` は例外で、再送 6 回、間隔 10 秒、上限 60 秒のポリシーを使います。オブジェクトを指定すると有効になり、`enabled: false` はこの既定ポリシーも無効にします。ストリーム開始前の HTTP 429 で `Retry-After` または固定間隔を待ち、キーの切り替え前に同じキーで同じリクエストを再送します。テキストターンの回復、Responses passthrough、画像・動画ブリッジ、web-search サイドカー、ターミナル継続が対象で、独自の `runTurn` は対象外です。`attempts` は最初の 429 後の同一キー再送回数(総送信数 = `attempts` + 1)で、リクエスト全体で共有します。使い切ると同一キーの再送だけが止まり、通常のキー切り替えか最終エラー処理に進みます。キー認証の passthrough では切り替えがなく、そのまま 429 を返します。正規の Mistral Chat で明示的な上流の待機時間が許容上限を超えると、同一キー再送を止めて元の `Retry-After` を返しますが、別のプールキーが処理できる場合があります。他のプロバイダーでは待機時間を上限で切り詰めます。Mistral は認証情報とモデルごとに期限を共有します。`requestPacing.enabled: false` または `retryOn429.respectRetryAfter: false` で無効になります。再試行時刻のない 429 で分単位の予算不足が検証されると、控えめな 60 秒の回復待機後、プローブを 1 回実行し、成功すれば通常のトラフィックを再開します。正常なトラフィックから次のトークン消費は予測しません。公表された許容量を使い、既知の月間枠の枯渇は除外し、正確なリセット時刻を保証せず、履歴の圧縮も起動しません。Codex 自体は 429 を再試行しません。オブジェクトの既定値:`enabled: true`、`attempts: 3`、`intervalMs: 5000`、`maxIntervalMs: 60000`(待機の上限、設定の最大値は 600000)、`respectRetryAfter: true`。 | | `transientRetryOn5xx?` | `{ enabled?: boolean; attempts?: number }` | キー認証の `openai-chat` および `openai-responses` プロバイダーのみ。`authMode: "forward"` のプロバイダー(ChatGPT アカウントプール)はこのオプションを読まず、既定の再試行段数を維持します。ストリーム開始前に上流から返される一時的なステータス(500、502、503、504、520、521、522)に対するオプトインの再試行です。設定がなければ無効で、オブジェクトを指定すると `enabled: false` でない限り有効になります。最初の Responses リクエスト、ターミナルガード継続、ネイティブの `/v1/chat/completions`、および 429/アカウント回復時の再取得が対象です。`attempts` は最初の送信を含め、1 回のリクエストで許可される上流への送信総数です(1~10、デフォルトは 3)。接続リセット回復と共有するリクエスト単位の単一予算であるため、`3` を指定した場合、プロバイダーに到達する実リクエストは最大 3 回です。待機には 400 ms を基準とする固定式の指数バックオフを使用し、上限は 5 秒で、`Retry-After` に従います。レート制限を扱う `retryOn429` とは別の機能であり、ストリーム開始後の失敗は再送されません。 各 Combo ターゲットでは、事前予約した初回送信もそのターゲットの設定総数に含まれます。`attempts: 1` は 1 回だけ送信し、予約を返却して余裕を作ったり、共有上限を広げたりしません。送信前のローカル拒否やキャンセルでは未使用の予約を解放します。 | -| `retryOnReset?` | `{ enabled?: boolean; replacements?: number }` | ネイティブ `openai-responses` プロバイダー(`authMode: "forward"` を含む)と、上流ヘッダー受信前の汎用 Responses 変換経路に適用されます。呼び出し側が何も観測しないまま失敗した送信を、オプトインで置き換えます。設定がなければ無効で、オブジェクトを指定すると `enabled: false` でない限り有効になります。レスポンスヘッダーが届く前に接続が切れた場合と、ヘッダー後に SSE 本文が制御イベントだけを運んだまま切れた場合の両方が対象です。canonical ChatGPT upstream WebSocket で create フレームの送信後、Responses イベントが届く前にソケットが閉じたかエラーになった場合も同じく対象で、その置き換えは HTTP で送信します。置き換えるのは自己完結したリクエストだけで、`store: false`、完全な `input`、`previous_response_id` / `conversation` / `stream_id` がないこと、クライアントが実行するツールのみ、が条件です。`replacements` は、すべてのレッグとすべてのコンボ子リクエストを合わせて 1 つの論理リクエストが行える置き換え送信の回数です(1..2、デフォルトは 1)。レッグ単位の再試行回数でも送信予算でもないため、置き換え送信もそのレッグがすでに持つ送信許容量に収まる必要があります。すでに出力やツール呼び出しを送ったリクエストは、この値に関わらず置き換えません。元の送信がすでに開始されていた場合は置き換えた推論も課金される可能性があるため、既定では無効です。 汎用変換経路では、初回送信と再構築後の送信のどちらも、同じ許可と送信予算でヘッダー受信前の接続リセットを置き換えます。アダプター独自のトランスポートと、上流ヘッダー受信後の変換ストリーム障害は対象外です。 | +| `retryOnReset?` | `{ enabled?: boolean; replacements?: number }` | ネイティブ `openai-responses` プロバイダー(`authMode: "forward"` を含む)と、上流ヘッダー受信前の汎用 Responses 変換経路に適用されます。呼び出し側が何も観測しないまま失敗した送信を、オプトインで置き換えます。設定がなければ無効で、オブジェクトを指定すると `enabled: false` でない限り有効になります。レスポンスヘッダーが届く前に接続が切れた場合と、ヘッダー後に SSE 本文が制御イベントだけを運んだまま切れた場合の両方が対象です。canonical ChatGPT upstream WebSocket で create フレームの送信後、Responses イベントが届く前にソケットが閉じたかエラーになった場合も同じく対象で、その置き換えは HTTP で送信します。置き換えるのは自己完結したリクエストだけで、`store: false`、完全な `input`、`previous_response_id` / `conversation` / `stream_id` がないこと、クライアントが実行するツールのみ、が条件です。このプロキシが自身のリプレイ状態から展開済みの `previous_response_id` を持つ連鎖ターンは、送信前にその ID が取り除かれるため対象になります。リセットが置き換えられなかった場合は、その理由(`ambiguous-no-policy`、`ambiguous-request-not-replayable`、`ambiguous-allowance-spent`)がサービスログに記録されます。`replacements` は、すべてのレッグとすべてのコンボ子リクエストを合わせて 1 つの論理リクエストが行える置き換え送信の回数です(1..2、デフォルトは 1)。レッグ単位の再試行回数でも送信予算でもないため、置き換え送信もそのレッグがすでに持つ送信許容量に収まる必要があります。すでに出力やツール呼び出しを送ったリクエストは、この値に関わらず置き換えません。元の送信がすでに開始されていた場合は置き換えた推論も課金される可能性があるため、既定では無効です。 汎用変換経路では、初回送信と再構築後の送信のどちらも、同じ許可と送信予算でヘッダー受信前の接続リセットを置き換えます。アダプター独自のトランスポートと、上流ヘッダー受信後の変換ストリーム障害は対象外です。 | | `autoToolChoiceOnlyModels?` | `string[]` | `tool_choice` が `auto` または `none` のみを受け入れるモデル。強制的な選択は格下げされます。 | | `preserveReasoningContentModels?` | `string[]` |チャット履歴に以前のアシスタント `reasoning_content` が必要なモデル。ダッシュボードから保存しても、保存済みのリスト(`[]` を含む)は保持されます。`PATCH /api/providers?name=` は配列、または消去するための `null` を受け付けます。 アダプター、ベース URL、または認証モードを変えて別の宛先に移す保存では保持されません(下の節を参照)。 | | `reasoningDetailsModels?` | `string[]` | thinking を構造化された `reasoning_details` 配列で返すモデル(`reasoning_split` 使用の MiniMax M シリーズ)。ストリーム差分は累積スナップショットとして prefix-diff され、保持された reasoning は `reasoning_content` 文字列ではなく `reasoning_details` 配列としてリプレイされます。 | diff --git a/docs-site/src/content/docs/ko/reference/configuration/providers.md b/docs-site/src/content/docs/ko/reference/configuration/providers.md index d05f2044ebc..7493327cc20 100644 --- a/docs-site/src/content/docs/ko/reference/configuration/providers.md +++ b/docs-site/src/content/docs/ko/reference/configuration/providers.md @@ -165,7 +165,7 @@ managed map을 활성화하면 privacy-safe selector를 만들고, 이후 계정 | `promptCacheKey?` | `boolean` | `openai-chat` 프로바이더 전체에서 `prompt_cache_key` 전달을 켭니다. 어댑터는 받은 키만 전달하지만 Claude Messages 변환은 `metadata.user_id` 또는 메타데이터가 없을 때 모델/시스템/도구 조합에서 키를 유도할 수 있습니다. 프리셋이 선언하지 않으면 기본적으로 꺼집니다. Mistral은 [프롬프트 캐시 계약](https://docs.mistral.ai/studio/conversations/advanced/prompt-caching)에 따라 켜며 명시적 `false`로 끕니다. 엄격한 게이트웨이는 알 수 없는 필드를 HTTP 400으로 거절할 수 있으므로 업스트림이 지원을 명시할 때만 켜세요. | | `retryOn429?` | `{ enabled?: boolean; attempts?: number; intervalMs?: number; maxIntervalMs?: number; respectRetryAfter?: boolean }` | API 키 프로바이더 전용(`authMode: "key"`). 미설정 시 꺼지지만 정식 엔드포인트의 OpenCode Go와 Command Code, 정식 Mistral `openai-chat`은 예외로 재전송 6회, 간격 10초, 상한 60초 정책을 사용합니다. 객체를 지정하면 켜지며 `enabled: false`는 이 기본 정책도 끕니다. 스트림 시작 전 HTTP 429에서 `Retry-After` 또는 고정 간격을 기다린 후 키 장애 조치 전에 동일 키로 동일 요청을 재전송합니다. 텍스트 턴 복구, Responses passthrough, 이미지/비디오 브리지, web-search 사이드카, 터미널 연속 요청을 포함하며 커스텀 `runTurn`은 제외됩니다. `attempts`는 첫 429 이후 동일 키 재전송 횟수(총 전송 = `attempts` + 1)로 요청 전체에서 공유됩니다. 소진 시 동일 키 재전송만 중단되고 일반 키 장애 조치나 최종 오류 처리가 이어집니다. 키 인증 passthrough에는 장애 조치가 없어 원래 429를 반환합니다. 정식 Mistral Chat의 명시적 대기 시간이 허용 상한을 넘으면 동일 키 재전송을 중단하고 원래 `Retry-After`를 반환하지만 풀의 다른 키가 처리할 수 있습니다. 다른 프로바이더는 대기 시간을 상한으로 제한합니다. Mistral은 자격 증명/모델별 재시도 기한을 공유하며 `requestPacing.enabled: false` 또는 `retryOn429.respectRetryAfter: false`로 끕니다. 재시도 시각이 없는 429에서 분당 예산 부족이 검증되면 보수적인 60초 복구 대기 후 한 번 탐색하고, 성공하면 일반 트래픽으로 돌아갑니다. 정상 트래픽에서는 다음 토큰 비용을 예측하지 않습니다. 공시된 허용량을 사용하고 소진된 월간 허용량은 제외하며 정확한 초기화 시각을 보장하거나 기록 압축을 유발하지 않습니다. Codex 자체는 429를 재시도하지 않습니다. 객체 기본값: `enabled: true`, `attempts: 3`, `intervalMs: 5000`, `maxIntervalMs: 60000`(대기 상한, 설정 최대 600000), `respectRetryAfter: true`. | | `transientRetryOn5xx?` | `{ enabled?: boolean; attempts?: number }` | 키 인증 `openai-chat` 및 `openai-responses` 프로바이더 전용입니다. `authMode: "forward"` 프로바이더(ChatGPT 계정 풀)는 이 옵션을 읽지 않고 기본 재시도 단계를 유지합니다. 스트림 시작 전의 일시적인 업스트림 상태(500, 502, 503, 504, 520, 521, 522)를 선택적으로 재시도합니다. 이 옵션이 없으면 꺼져 있고, 객체가 있으면 `enabled: false`가 아닌 한 활성화됩니다. 최초 Responses 요청, 터미널 가드 연속 요청, 네이티브 `/v1/chat/completions`, 429/계정 복구 재조회를 포함합니다. `attempts`는 최초 전송을 포함하여 요청 하나에 허용되는 업스트림 전송의 총횟수(1..10, 기본값 3)입니다. 연결 재설정 복구와 요청 단위 예산 하나를 공유하므로 `3`이면 실제로 프로바이더에 도달하는 요청은 최대 세 번입니다. 대기에는 400ms로 고정된 지수 백오프를 사용하고 상한은 5초이며 `Retry-After`를 따릅니다. 속도 제한을 처리하는 `retryOn429`와는 별개이며, 스트림 도중의 실패는 절대 재전송하지 않습니다. 각 Combo 대상은 선예약한 첫 전송을 해당 대상의 설정 총횟수에 포함합니다. `attempts: 1`은 한 번만 전송하며, 예약을 환불해 여유를 만들거나 공유 상한을 늘리지 않습니다. 전송 전 로컬 거부나 취소는 미사용 예약을 해제합니다. | -| `retryOnReset?` | `{ enabled?: boolean; replacements?: number }` | 네이티브 `openai-responses` 프로바이더(`authMode: "forward"` 포함)와 업스트림 헤더 수신 전의 일반 Responses 변환 전송 경로에 적용됩니다. 호출자가 아무것도 관측하지 못한 채 실패한 전송을 선택적으로 대체합니다. 이 옵션이 없으면 꺼져 있고, 객체가 있으면 `enabled: false`가 아닌 한 활성화됩니다. 응답 헤더가 오기 전에 연결이 끊어진 경우와, 헤더 이후 SSE 본문이 제어 이벤트만 실은 채 끊어진 경우를 모두 다룹니다. canonical ChatGPT 업스트림 WebSocket에서 create 프레임을 보낸 뒤 Responses 이벤트가 오기 전에 소켓이 닫히거나 오류가 난 경우도 같은 방식으로 다루며, 이때 대체 전송은 HTTP로 보냅니다. 자체 완결된 요청만 대체합니다. `store: false`, 완전한 `input`, `previous_response_id`·`conversation`·`stream_id` 없음, 클라이언트가 실행하는 도구만 해당합니다. `replacements`는 모든 구간과 모든 콤보 자식을 합쳐 논리 요청 하나가 만들 수 있는 대체 전송 횟수입니다(1..2, 기본값 1). 구간별 재시도 횟수도 전송 예산도 아니므로, 대체 전송도 해당 구간이 이미 가진 전송 허용량 안에 들어가야 합니다. 이미 출력이나 도구 호출을 내보낸 요청은 이 값과 무관하게 대체하지 않습니다. 원본 전송이 이미 시작됐다면 대체한 추론도 과금될 수 있어서 기본값은 꺼짐입니다. 일반 변환 전송 경로에서는 최초 전송과 재구축한 전송 모두 같은 대체 권한과 전송 예산으로 헤더 수신 전 연결 리셋을 대체합니다. 어댑터가 자체 관리하는 전송 경로와 업스트림 헤더 수신 후 변환 스트림 장애는 제외됩니다. | +| `retryOnReset?` | `{ enabled?: boolean; replacements?: number }` | 네이티브 `openai-responses` 프로바이더(`authMode: "forward"` 포함)와 업스트림 헤더 수신 전의 일반 Responses 변환 전송 경로에 적용됩니다. 호출자가 아무것도 관측하지 못한 채 실패한 전송을 선택적으로 대체합니다. 이 옵션이 없으면 꺼져 있고, 객체가 있으면 `enabled: false`가 아닌 한 활성화됩니다. 응답 헤더가 오기 전에 연결이 끊어진 경우와, 헤더 이후 SSE 본문이 제어 이벤트만 실은 채 끊어진 경우를 모두 다룹니다. canonical ChatGPT 업스트림 WebSocket에서 create 프레임을 보낸 뒤 Responses 이벤트가 오기 전에 소켓이 닫히거나 오류가 난 경우도 같은 방식으로 다루며, 이때 대체 전송은 HTTP로 보냅니다. 자체 완결된 요청만 대체합니다. `store: false`, 완전한 `input`, `previous_response_id`·`conversation`·`stream_id` 없음, 클라이언트가 실행하는 도구만 해당합니다. 이 프록시가 자체 재생 상태로 이미 확장한 `previous_response_id`가 있는 연쇄 턴은 전송 전에 그 ID가 제거되므로 대상이 됩니다. 리셋이 대체되지 않으면 서비스 로그에 그 이유(`ambiguous-no-policy`, `ambiguous-request-not-replayable`, `ambiguous-allowance-spent`)가 기록됩니다. `replacements`는 모든 구간과 모든 콤보 자식을 합쳐 논리 요청 하나가 만들 수 있는 대체 전송 횟수입니다(1..2, 기본값 1). 구간별 재시도 횟수도 전송 예산도 아니므로, 대체 전송도 해당 구간이 이미 가진 전송 허용량 안에 들어가야 합니다. 이미 출력이나 도구 호출을 내보낸 요청은 이 값과 무관하게 대체하지 않습니다. 원본 전송이 이미 시작됐다면 대체한 추론도 과금될 수 있어서 기본값은 꺼짐입니다. 일반 변환 전송 경로에서는 최초 전송과 재구축한 전송 모두 같은 대체 권한과 전송 예산으로 헤더 수신 전 연결 리셋을 대체합니다. 어댑터가 자체 관리하는 전송 경로와 업스트림 헤더 수신 후 변환 스트림 장애는 제외됩니다. | | `autoToolChoiceOnlyModels?` | `string[]` | `tool_choice`가 `auto` 또는 `none`만 받는 모델입니다. 강제 선택은 낮은 수준으로 바뀝니다. | | `preserveReasoningContentModels?` | `string[]` | chat 기록에서 이전 assistant `reasoning_content`가 필요한 모델입니다. 대시보드에서 저장해도 저장된 목록(`[]` 포함)은 유지됩니다. `PATCH /api/providers?name=`는 배열 또는 지우기 위한 `null`을 받습니다. 어댑터, 기본 URL, 인증 모드를 바꿔 다른 목적지로 옮기는 저장에서는 유지되지 않습니다(아래 절 참고). | | `reasoningDetailsModels?` | `string[]` | thinking을 구조화된 `reasoning_details` 배열로 반환하는 모델(`reasoning_split` 사용 MiniMax M 시리즈). 스트림 델타는 누적 스냅샷이라 prefix-diff로 처리하고, 보존된 reasoning은 `reasoning_content` 문자열 대신 `reasoning_details` 배열로 리플레이합니다. | diff --git a/docs-site/src/content/docs/reference/configuration/providers.md b/docs-site/src/content/docs/reference/configuration/providers.md index 0f7b1982f08..f8a5a35d954 100644 --- a/docs-site/src/content/docs/reference/configuration/providers.md +++ b/docs-site/src/content/docs/reference/configuration/providers.md @@ -419,7 +419,7 @@ Providers can expose a built-in shorthand, such as `agy` for `google-antigravity | `webSearchBridge?` | `{ enabled?: boolean; backend?: "ollama" \| "openai" \| "anthropic" \| "xai" \| "gemini" \| "exa"; maxSearches?: number; timeoutMs?: number; endpoint?: string }` | Key-auth `openai-responses` passthrough providers only. Off by default. Codex always declares the hosted `web_search` tool, and the passthrough relays it on the assumption the destination executes it. A gateway that does not run hosted search answers with a `function_call` named `web_search` that nothing runs, and the undeclared-tool guard ends the turn. With `enabled: true` and an explicit `backend` OpenCodex intercepts that call, runs the search itself, feeds the result back to the same upstream, and shows Codex a hosted `web_search_call` cell. Never armed for `authMode: "forward"` (ChatGPT already searches) or for a provider that executes hosted search upstream. `backend` is required; there is no implicit default and a missing credential for the named backend leaves the bridge disarmed rather than falling through to another paid search. `ollama` reuses this provider's own API key on `POST /api/web_search`, so the origin must be `https://ollama.com` unless the operator names `endpoint` explicitly. `openai` / `anthropic` / `xai` / `gemini` / `exa` reuse the matching sidecar executor and that executor's own credential (`webSearchSidecar.exaApiKey` for Exa). The search model comes from `webSearchSidecar.model` only when `webSearchSidecar.backend` resolves to the same backend this bridge names; otherwise the bridge runs that backend's own default, because a model chosen for one vendor is rejected by another. An unset `webSearchSidecar.backend` resolves to `openai`, so an unset-backend model reaches an `openai` bridge and no other. There is no per-provider bridge model override. Streaming turns only. A turn that mixes `web_search` with another client tool call still fails closed rather than dropping the client's call. Assistant text such as XML-like `` prose is not executed. Defaults: `maxSearches: 3` (1..10), `timeoutMs: 60000` (1000..600000). | | `retryOn429?` | `{ enabled?: boolean; attempts?: number; intervalMs?: number; maxIntervalMs?: number; respectRetryAfter?: boolean }` | API-key providers only (`authMode: "key"`). Opt-in same-target 429 retry: when `retryOn429` is absent the feature is off, except that key-auth OpenCode Go and Command Code (canonical endpoints) fall back to a patient policy (6 replays, 10 s interval, 60 s cap), and canonical Mistral `openai-chat` falls back to 6 replays with a 10 s interval and 60 s cap; object presence enables it unless `enabled: false`, which also turns that fallback off. On 429 the proxy waits (upstream `Retry-After` or the fixed interval) and replays the identical request on the same key before any key failover — across the main text-turn recovery loop, the Responses passthrough wire, the image/video bridge, the web-search sidecar, and terminal continuations. Only pre-stream HTTP 429 responses are eligible for replay; custom `runTurn` transports are outside the HTTP retry loop. `attempts` counts same-key replays after the first 429 (total sends = `attempts` + 1) and is one request-wide budget shared by the main recovery loop, the terminal-guard continuation, and bridge retries. Exhausting `attempts` only stops further same-key replays: normal key failover or final-error handling then applies per the available targets — on the key-auth passthrough wire there is no failover, so the exhausted 429 surfaces as-is. For canonical Mistral Chat, an explicit upstream delay above the allowed wait stops same-key replay (another pooled key may still serve) and returns the original `Retry-After`; other providers retain the capped-delay behavior. Mistral shares retry deadlines per credential/model unless `requestPacing.enabled: false` or `retryOn429.respectRetryAfter: false`. A 429 with validated minute-budget pressure and no retry time activates a conservative 60 s recovery wait, followed by one probe; success restores ordinary traffic. Healthy traffic never predicts the next token charge. This policy uses advertised allowances, excludes known exhausted monthly allowances, does not claim an exact reset time, and never triggers compaction. Codex itself never retries 429, so this is the only defense for single-key providers. Defaults: `enabled: true`, `attempts: 3`, `intervalMs: 5000`, `maxIntervalMs: 60000` (any single wait is capped at `maxIntervalMs`, itself capped at 600000), `respectRetryAfter: true`. | | `transientRetryOn5xx?` | `{ enabled?: boolean; attempts?: number }` | Key-auth `openai-chat` and `openai-responses` providers only. `authMode: "forward"` providers (the ChatGPT account pool) never read this option and keep the default ladder. Opt-in retry for pre-stream transient upstream statuses (500, 502, 503, 504, 520, 521, 522): absent means off, object presence enables it unless `enabled: false`. Covers the initial Responses request, the Responses passthrough lane and each of its recovery legs (OAuth-401 replay, same-target 429 replay, validated rebuild), the terminal-guard continuation, and native `/v1/chat/completions`. `attempts` is the TOTAL number of upstream sends allowed for one request including the first (1..10, default 3) — it is one budget shared with connection-reset recovery, so `3` means at most three real requests reach the provider. On the Responses passthrough lane the configured value is additionally intersected with the request-wide send allowance, so a value below that allowance narrows the ladder exactly while a value above it does not raise the bound. Each Combo target includes its prepaid first send in that target's configured total; `attempts: 1` sends once, without refunding the reservation or widening the shared Combo ceiling. A local refusal or cancellation before dispatch releases an unsent booking. Waits use a fixed 400 ms exponential backoff capped at 5 s and honor `Retry-After`. Separate from `retryOn429`, which handles rate limiting; mid-stream failures are never replayed. | -| `retryOnReset?` | `{ enabled?: boolean; replacements?: number }` | Native `openai-responses` providers, including `authMode: "forward"`, plus generic translated Responses dispatch before upstream headers. Opt-in replacement of a send that failed while the caller had observed nothing: absent means off, object presence enables it unless `enabled: false`. Covers both ambiguous stages — a connection that died before any response header, and an SSE body that died after the header while carrying only control events. A canonical ChatGPT upstream WebSocket that closed or errored after its create frame left, before any Responses event, is covered the same way, and its replacement is sent over HTTP. Only a self-contained request is ever replaced: `store: false`, complete `input`, no `previous_response_id`, `conversation` or `stream_id`, and only client-executed tools. `replacements` is the number of replacement sends ONE logical request may make across every leg and every combo child (1..2, default 1) — not a per-leg retry count and not a send budget, so a replacement still has to fit inside the send allowance the leg already had. A request that already emitted output or a tool call is never replaced, whatever this is set to. The replacement inference may still be billed if the origin had already started the first one, which is why this is off by default. Generic translated dispatch supports pre-header reset replacement for initial and rebuilt sends under the same grant and send budget. Adapter-owned transports and translated stream failures after upstream headers are excluded. | +| `retryOnReset?` | `{ enabled?: boolean; replacements?: number }` | Native `openai-responses` providers, including `authMode: "forward"`, plus generic translated Responses dispatch before upstream headers. Opt-in replacement of a send that failed while the caller had observed nothing: absent means off, object presence enables it unless `enabled: false`. Covers both ambiguous stages — a connection that died before any response header, and an SSE body that died after the header while carrying only control events. A canonical ChatGPT upstream WebSocket that closed or errored after its create frame left, before any Responses event, is covered the same way, and its replacement is sent over HTTP. Only a self-contained request is ever replaced: `store: false`, complete `input`, no `previous_response_id`, `conversation` or `stream_id`, and only client-executed tools. A chained turn whose `previous_response_id` this proxy already expanded from its own replay state qualifies, because that id is removed before the send. When a reset is not replaced, the service log records why: `ambiguous-no-policy`, `ambiguous-request-not-replayable` or `ambiguous-allowance-spent`. `replacements` is the number of replacement sends ONE logical request may make across every leg and every combo child (1..2, default 1) — not a per-leg retry count and not a send budget, so a replacement still has to fit inside the send allowance the leg already had. A request that already emitted output or a tool call is never replaced, whatever this is set to. The replacement inference may still be billed if the origin had already started the first one, which is why this is off by default. Generic translated dispatch supports pre-header reset replacement for initial and rebuilt sends under the same grant and send budget. Adapter-owned transports and translated stream failures after upstream headers are excluded. | | `autoToolChoiceOnlyModels?` | `string[]` | Models whose `tool_choice` accepts only `auto` or `none`; forced choices are downgraded. | | `preserveReasoningContentModels?` | `string[]` | Models requiring prior assistant `reasoning_content` in chat history. Translated Chat requests to `api.mistral.ai` instead reconstruct native thinking chunks before assistant text; supplied thinking replays by default for all hosted families, and an explicit list (including `[]`) overrides enrollment. No placeholder thinking or signatures are invented. Native Chat passthrough is unchanged. A provider save that keeps the destination keeps the stored list, including `[]`; see [What a provider save keeps](#what-a-provider-save-keeps). `PATCH /api/providers?name=` accepts an array or `null` to clear it. | | `inlineThinkTagModels?` | `string[]` | Opt-in recovery for `openai-chat` gateways without a server-side reasoning parser. A leading `` / `` / `` block (optionally after whitespace) activates splitting in streamed and buffered replies. All answer whitespace is preserved. Subsequent tags are delimiters anywhere, including same-line interleaving and code fences; this mode does not interpret Markdown. Ordinary text or a code fence before the first tag keeps the whole reply untouched. Off by default; prefer structured upstream reasoning or `reasoningSplitModels` where supported. | diff --git a/docs-site/src/content/docs/ru/reference/configuration/providers.md b/docs-site/src/content/docs/ru/reference/configuration/providers.md index 93c92cc44cc..85caeca0641 100644 --- a/docs-site/src/content/docs/ru/reference/configuration/providers.md +++ b/docs-site/src/content/docs/ru/reference/configuration/providers.md @@ -178,7 +178,7 @@ cross-route credential fallback не существует. Строки API GPT- | `promptCacheKey?` | `boolean` | Включает передачу `prompt_cache_key` для всего провайдера `openai-chat`. Адаптер передаёт полученный ключ, не создавая свой, но перевод Claude Messages может вывести его из `metadata.user_id` либо из сочетания модели/системы/инструментов без метаданных. По умолчанию выключено, если пресет не указывает иное; Mistral включает по своему [контракту кэширования](https://docs.mistral.ai/studio/conversations/advanced/prompt-caching), явное `false` отключает. Включайте только при документированной поддержке апстримом: строгий шлюз может отклонить неизвестное поле с HTTP 400. | | `retryOn429?` | `{ enabled?: boolean; attempts?: number; intervalMs?: number; maxIntervalMs?: number; respectRetryAfter?: boolean }` | Только API-ключи (`authMode: "key"`). Без настройки выключено, кроме канонических OpenCode Go и Command Code и канонического Mistral с `openai-chat`: 6 повторов, интервал 10 с, предел 60 с. Объект включает повторы; `enabled: false` отключает и эти значения по умолчанию. При HTTP 429 до начала потока прокси ждёт `Retry-After` или фиксированный интервал и повторяет тот же запрос с тем же ключом до переключения ключей. Охватывает восстановление текстового хода, Responses passthrough, мосты изображений/видео, web-search и терминальные продолжения; пользовательский `runTurn` исключён. `attempts` — число повторов после первого 429 (всего отправок = `attempts` + 1), общий бюджет всего запроса. Исчерпание прекращает только повторы с тем же ключом, затем применяется обычное переключение или окончательная ошибка; passthrough с ключом не переключается и возвращает 429 как есть. Для канонического Mistral Chat явная задержка сверх разрешённого ожидания прекращает повтор с тем же ключом и сохраняет исходный `Retry-After`; другой ключ пула ещё может обслужить запрос. У остальных провайдеров задержка ограничивается пределом. Mistral разделяет сроки между вызовами с одинаковыми учётными данными и моделью, кроме `requestPacing.enabled: false` или `retryOn429.respectRetryAfter: false`. 429 без времени повтора при подтверждённом дефиците минутного бюджета запускает консервативное ожидание 60 с и одну пробную попытку; успех возвращает обычный трафик. Здоровый трафик не предсказывает следующую стоимость токенов. Политика использует заявленные лимиты, исключает известное исчерпание месячных лимитов, не обещает точное время сброса и не запускает сжатие истории. Сам Codex не повторяет 429. Значения объекта по умолчанию: `enabled: true`, `attempts: 3`, `intervalMs: 5000`, `maxIntervalMs: 60000` (предел ожидания, максимум настройки 600000), `respectRetryAfter: true`. | | `transientRetryOn5xx?` | `{ enabled?: boolean; attempts?: number }` | Только для провайдеров `openai-chat` и `openai-responses` с аутентификацией по ключу. Провайдеры с `authMode: "forward"` (пул аккаунтов ChatGPT) никогда не читают эту настройку и сохраняют число повторов по умолчанию. Опциональный повтор при временных статусах апстрима до начала потока (500, 502, 503, 504, 520, 521, 522): если параметр отсутствует, функция выключена; наличие объекта включает её, если только `enabled: false`. Покрывает исходный запрос `Responses`, продолжение терминального предохранителя, нативный `/v1/chat/completions`, а также повторные запросы при восстановлении после 429 или ошибки учётной записи. `attempts` — ОБЩЕЕ число разрешённых отправок в апстрим для одного запроса, включая первую (1..10, по умолчанию 3). Это единый бюджет на запрос, общий с восстановлением после сброса соединения, поэтому `3` означает, что до провайдера дойдут не более трёх реальных запросов. Ожидание использует экспоненциальную задержку с фиксированной начальной величиной 400 мс, ограниченную 5 с, и учитывает `Retry-After`. Параметр не связан с `retryOn429`, который обрабатывает ограничение частоты запросов; сбои после начала потока никогда не воспроизводятся. Для каждой цели Combo первая заранее зарезервированная отправка входит в настроенный итог этой цели: `attempts: 1` допускает одну отправку, без возврата резерва ради дополнительного лимита и без расширения общего потолка. Локальный отказ или отмена до отправки освобождает неиспользованный резерв. | -| `retryOnReset?` | `{ enabled?: boolean; replacements?: number }` | Для нативных провайдеров `openai-responses`, включая `authMode: "forward"`, а также для общего пути отправки переведённых запросов Responses до получения заголовков от upstream. Необязательная замена отправки, которая завершилась неудачей, когда вызывающая сторона ещё ничего не наблюдала: если параметр отсутствует, функция выключена; наличие объекта включает её, если только `enabled: false`. Покрывает обе неоднозначные стадии — обрыв соединения до любого заголовка ответа и обрыв тела SSE после заголовка, когда оно несло только управляющие события. Так же покрывается upstream WebSocket canonical ChatGPT, который закрылся или завершился ошибкой после отправки кадра create и до любого события Responses; замена в этом случае отправляется по HTTP. Заменяется только самодостаточный запрос: `store: false`, полный `input`, отсутствие `previous_response_id`, `conversation` и `stream_id`, и только инструменты, исполняемые клиентом. `replacements` — число замещающих отправок, которые ОДИН логический запрос может сделать по всем участкам и всем дочерним запросам комбо (1..2, по умолчанию 1). Это не число повторов на участок и не бюджет отправок, поэтому замена всё равно должна поместиться в уже имеющийся у участка лимит отправок. Запрос, который уже выдал вывод или вызов инструмента, не заменяется никогда, каким бы ни было это значение. Замещающий вывод модели всё равно может быть оплачен, если источник уже начал первый, поэтому параметр выключен по умолчанию. Общий путь перевода поддерживает замену при сбросе соединения до заголовков для первоначальных и заново сформированных отправок, используя то же разрешение и бюджет отправок. Транспорты, управляемые самими адаптерами, и сбои переведённого потока после получения заголовков от upstream исключены. | +| `retryOnReset?` | `{ enabled?: boolean; replacements?: number }` | Для нативных провайдеров `openai-responses`, включая `authMode: "forward"`, а также для общего пути отправки переведённых запросов Responses до получения заголовков от upstream. Необязательная замена отправки, которая завершилась неудачей, когда вызывающая сторона ещё ничего не наблюдала: если параметр отсутствует, функция выключена; наличие объекта включает её, если только `enabled: false`. Покрывает обе неоднозначные стадии — обрыв соединения до любого заголовка ответа и обрыв тела SSE после заголовка, когда оно несло только управляющие события. Так же покрывается upstream WebSocket canonical ChatGPT, который закрылся или завершился ошибкой после отправки кадра create и до любого события Responses; замена в этом случае отправляется по HTTP. Заменяется только самодостаточный запрос: `store: false`, полный `input`, отсутствие `previous_response_id`, `conversation` и `stream_id`, и только инструменты, исполняемые клиентом. Цепочечный ход, чей `previous_response_id` прокси уже развернул из собственного состояния воспроизведения, подходит: этот идентификатор удаляется перед отправкой. Если сброс не заменён, журнал службы записывает причину: `ambiguous-no-policy`, `ambiguous-request-not-replayable` или `ambiguous-allowance-spent`. `replacements` — число замещающих отправок, которые ОДИН логический запрос может сделать по всем участкам и всем дочерним запросам комбо (1..2, по умолчанию 1). Это не число повторов на участок и не бюджет отправок, поэтому замена всё равно должна поместиться в уже имеющийся у участка лимит отправок. Запрос, который уже выдал вывод или вызов инструмента, не заменяется никогда, каким бы ни было это значение. Замещающий вывод модели всё равно может быть оплачен, если источник уже начал первый, поэтому параметр выключен по умолчанию. Общий путь перевода поддерживает замену при сбросе соединения до заголовков для первоначальных и заново сформированных отправок, используя то же разрешение и бюджет отправок. Транспорты, управляемые самими адаптерами, и сбои переведённого потока после получения заголовков от upstream исключены. | | `autoToolChoiceOnlyModels?` | `string[]` | Модели, у которых `tool_choice` принимает только `auto` или `none`; forced choice понижается. | | `preserveReasoningContentModels?` | `string[]` | Модели, которым нужен предыдущий assistant `reasoning_content` в chat history. Сохранение в дашборде не меняет записанный список, в том числе `[]`. `PATCH /api/providers?name=` принимает массив или `null`, чтобы его очистить. Сохранение, которое переносит провайдера на другой адаптер, базовый URL или режим аутентификации, список не сохраняет (см. раздел ниже). | | `reasoningDetailsModels?` | `string[]` | Модели, чей endpoint возвращает thinking как структурированный массив `reasoning_details` (MiniMax M-series с `reasoning_split`); потоковые дельты — кумулятивные снимки, сравниваемые по префиксу, а сохранённый reasoning воспроизводится массивом `reasoning_details` вместо строки `reasoning_content`. | diff --git a/docs-site/src/content/docs/tr/reference/configuration/providers.md b/docs-site/src/content/docs/tr/reference/configuration/providers.md index c8db68a57eb..638c0fef3e6 100644 --- a/docs-site/src/content/docs/tr/reference/configuration/providers.md +++ b/docs-site/src/content/docs/tr/reference/configuration/providers.md @@ -177,7 +177,7 @@ alanlı seçilmiş kimlikleri yalın kimliklere yeniden yazar. | `promptCacheKey?` | `boolean` | `openai-chat` sağlayıcısı genelinde `prompt_cache_key` iletimini etkinleştirir. Adaptör aldığı anahtarı iletir, üretmez; ancak Claude Messages çevirisi `metadata.user_id` veya metadata yoksa model/sistem/araç grubundan anahtar türetebilir. Ön ayar belirtmedikçe varsayılan kapalıdır; Mistral [istem önbelleği sözleşmesine](https://docs.mistral.ai/studio/conversations/advanced/prompt-caching) göre açar, açık `false` kapatır. Yalnızca üst akış desteği belgeliyse açın; katı ağ geçitleri bilinmeyen alanı HTTP 400 ile reddedebilir. | | `retryOn429?` | `{ enabled?: boolean; attempts?: number; intervalMs?: number; maxIntervalMs?: number; respectRetryAfter?: boolean }` | Yalnızca API anahtarlı sağlayıcılar (`authMode: "key"`). Ayar yoksa kapalıdır; kanonik OpenCode Go ve Command Code ile kanonik Mistral `openai-chat` istisnadır: 6 tekrar, 10 sn aralık, 60 sn tavan. Nesne verilmesi etkinleştirir; `enabled: false` bu varsayılanları da kapatır. Akış öncesi HTTP 429 durumunda `Retry-After` veya sabit aralık beklenir ve anahtar değişiminden önce aynı istek aynı anahtarla yeniden gönderilir. Metin turu kurtarma, Responses passthrough, görüntü/video köprüsü, web-search yan hizmeti ve terminal devamlarını kapsar; özel `runTurn` taşıması kapsam dışıdır. `attempts`, ilk 429 sonrasındaki aynı anahtar tekrarlarıdır (toplam gönderim = `attempts` + 1) ve istek genelinde paylaşılır. Tükenmesi yalnızca aynı anahtar tekrarını durdurur; normal anahtar değişimi veya son hata işleme sürer. Anahtarlı passthrough yolunda geçiş yoktur, 429 aynen döner. Kanonik Mistral Chat için açık üst akış gecikmesi izin verilen beklemeyi aşarsa aynı anahtar tekrarı durur ve özgün `Retry-After` korunur; havuzdaki başka anahtar yine hizmet verebilir. Diğer sağlayıcılarda gecikme tavana kırpılır. Mistral, kimlik bilgisi/model başına son tarihleri paylaşır; `requestPacing.enabled: false` veya `retryOn429.respectRetryAfter: false` bunu kapatır. Tekrar zamanı olmayan 429 ve doğrulanmış dakika bütçesi baskısı, temkinli 60 sn bekleme ve ardından tek kurtarma denemesi başlatır; başarı olağan trafiği geri getirir. Sağlıklı trafik sonraki token maliyetini tahmin etmez. Bildirilen kotalar kullanılır; tükendiği bilinen aylık kotalar hariçtir, kesin sıfırlama zamanı vaat edilmez ve geçmiş sıkıştırılmaz. Codex 429’u kendisi yeniden denemez. Nesne varsayılanları: `enabled: true`, `attempts: 3`, `intervalMs: 5000`, `maxIntervalMs: 60000` (bekleme tavanı, ayar en fazla 600000), `respectRetryAfter: true`. | | `transientRetryOn5xx?` | `{ enabled?: boolean; attempts?: number }` | Yalnızca anahtarla kimlik doğrulanan `openai-chat` ve `openai-responses` sağlayıcıları. `authMode: "forward"` sağlayıcıları (ChatGPT hesap havuzu) bu seçeneği hiç okumaz ve varsayılan merdiveni korur. Akış öncesi geçici yukarı akış durumları (500, 502, 503, 504, 520, 521, 522) için isteğe bağlı yeniden deneme: seçenek belirtilmezse kapalıdır; nesnenin varlığı, `enabled: false` olmadığı sürece özelliği etkinleştirir. İlk Responses isteğini, terminal koruma devamını, yerel `/v1/chat/completions` isteklerini ve 429/hesap kurtarma yeniden getirmelerini kapsar. `attempts`, bir istek için ilk gönderim dahil izin verilen yukarı akış gönderimlerinin TOPLAM sayısıdır (1..10, varsayılan 3) — bağlantı sıfırlama kurtarmasıyla paylaşılan, istek kapsamlı tek bütçedir; dolayısıyla `3`, sağlayıcıya en fazla üç gerçek isteğin ulaşması anlamına gelir. Beklemelerde 400 ms'lik sabit üstel geri çekilme uygulanır, süre 5 sn ile sınırlandırılır ve `Retry-After` dikkate alınır. Hız sınırlamasını işleyen `retryOn429` seçeneğinden ayrıdır; akış ortası hataları hiçbir zaman yeniden oynatılmaz. Her Combo hedefinde önceden ayrılmış ilk gönderim, o hedefin yapılandırılmış toplamına dahildir: `attempts: 1` tek gönderim yapar; rezervasyonu geri vererek ek hak yaratmaz ve ortak tavanı artırmaz. Gönderim öncesi yerel ret veya iptal, kullanılmamış rezervasyonu serbest bırakır. | -| `retryOnReset?` | `{ enabled?: boolean; replacements?: number }` | Yerel `openai-responses` sağlayıcıları (`authMode: "forward"` dahil) ve upstream başlıkları gelmeden önceki genel Responses çeviri gönderim yolu için geçerlidir. Çağıranın hiçbir şey gözlemlemediği bir anda başarısız olan gönderimin isteğe bağlı olarak değiştirilmesi: seçenek belirtilmezse kapalıdır; nesnenin varlığı, `enabled: false` olmadığı sürece özelliği etkinleştirir. İki belirsiz aşamayı da kapsar: yanıt başlığı gelmeden kopan bağlantı ve başlıktan sonra yalnızca denetim olayları taşırken kopan SSE gövdesi. Kanonik ChatGPT upstream WebSocket'i create çerçevesi gönderildikten sonra, herhangi bir Responses olayından önce kapanır veya hata verirse aynı şekilde kapsanır ve değiştirme gönderimi HTTP üzerinden yapılır. Yalnızca kendi kendine yeten bir istek değiştirilir: `store: false`, eksiksiz `input`, `previous_response_id`, `conversation` veya `stream_id` bulunmaması ve yalnızca istemcinin yürüttüğü araçlar. `replacements`, BİR mantıksal isteğin tüm bacaklar ve tüm combo alt istekleri boyunca yapabileceği değiştirme gönderimi sayısıdır (1..2, varsayılan 1). Bacak başına yeniden deneme sayısı da gönderim bütçesi de değildir; bu yüzden bir değiştirme gönderimi, ilgili bacağın hâlihazırda sahip olduğu gönderim payına sığmak zorundadır. Halihazırda çıktı veya araç çağrısı üretmiş bir istek, bu değer ne olursa olsun asla değiştirilmez. Kaynak ilk çıkarımı zaten başlatmışsa değiştirilen çıkarım yine ücretlendirilebilir; bu nedenle varsayılan olarak kapalıdır. Genel çeviri gönderim yolu, ilk ve yeniden oluşturulan gönderimlerde başlık öncesi bağlantı sıfırlamasından sonra aynı izin ve gönderim bütçesiyle değiştirme destekler. Adaptörlerin kendi yönettiği taşıma yolları ve upstream başlıklarından sonraki çevrilmiş akış hataları kapsam dışıdır. | +| `retryOnReset?` | `{ enabled?: boolean; replacements?: number }` | Yerel `openai-responses` sağlayıcıları (`authMode: "forward"` dahil) ve upstream başlıkları gelmeden önceki genel Responses çeviri gönderim yolu için geçerlidir. Çağıranın hiçbir şey gözlemlemediği bir anda başarısız olan gönderimin isteğe bağlı olarak değiştirilmesi: seçenek belirtilmezse kapalıdır; nesnenin varlığı, `enabled: false` olmadığı sürece özelliği etkinleştirir. İki belirsiz aşamayı da kapsar: yanıt başlığı gelmeden kopan bağlantı ve başlıktan sonra yalnızca denetim olayları taşırken kopan SSE gövdesi. Kanonik ChatGPT upstream WebSocket'i create çerçevesi gönderildikten sonra, herhangi bir Responses olayından önce kapanır veya hata verirse aynı şekilde kapsanır ve değiştirme gönderimi HTTP üzerinden yapılır. Yalnızca kendi kendine yeten bir istek değiştirilir: `store: false`, eksiksiz `input`, `previous_response_id`, `conversation` veya `stream_id` bulunmaması ve yalnızca istemcinin yürüttüğü araçlar. Bu proxy'nin kendi yeniden oynatma durumundan zaten genişlettiği bir `previous_response_id` taşıyan zincirleme tur uygundur, çünkü bu kimlik gönderimden önce kaldırılır. Bir sıfırlama değiştirilmediğinde hizmet günlüğü nedenini kaydeder: `ambiguous-no-policy`, `ambiguous-request-not-replayable` veya `ambiguous-allowance-spent`. `replacements`, BİR mantıksal isteğin tüm bacaklar ve tüm combo alt istekleri boyunca yapabileceği değiştirme gönderimi sayısıdır (1..2, varsayılan 1). Bacak başına yeniden deneme sayısı da gönderim bütçesi de değildir; bu yüzden bir değiştirme gönderimi, ilgili bacağın hâlihazırda sahip olduğu gönderim payına sığmak zorundadır. Halihazırda çıktı veya araç çağrısı üretmiş bir istek, bu değer ne olursa olsun asla değiştirilmez. Kaynak ilk çıkarımı zaten başlatmışsa değiştirilen çıkarım yine ücretlendirilebilir; bu nedenle varsayılan olarak kapalıdır. Genel çeviri gönderim yolu, ilk ve yeniden oluşturulan gönderimlerde başlık öncesi bağlantı sıfırlamasından sonra aynı izin ve gönderim bütçesiyle değiştirme destekler. Adaptörlerin kendi yönettiği taşıma yolları ve upstream başlıklarından sonraki çevrilmiş akış hataları kapsam dışıdır. | | `autoToolChoiceOnlyModels?` | `string[]` | `tool_choice`'u yalnızca `auto` veya `none` kabul eden modeller; zorunlu seçimlerin derecesi düşürülür. | | `preserveReasoningContentModels?` | `string[]` | Sohbet geçmişinde önceki asistan `reasoning_content`'ini gerektiren modeller. Dashboard üzerinden yapılan kayıtlar, `[]` dahil saklanan listeyi korur. `PATCH /api/providers?name=`, bir dizi veya temizlemek için `null` kabul eder. Sağlayıcıyı başka bir bağdaştırıcıya, temel URL'ye veya kimlik doğrulama moduna taşıyan bir kayıt listeyi korumaz (aşağıdaki bölüme bakın). | | `reasoningDetailsModels?` | `string[]` | Thinking'i yapılandırılmış bir `reasoning_details` dizisi olarak döndüren modeller (`reasoning_split` ile MiniMax M-serisi); akış deltaları önek farkıyla işlenen kümülatif anlık görüntülerdir ve korunan reasoning, `reasoning_content` dizesi yerine `reasoning_details` dizisi olarak yeniden oynatılır. | diff --git a/docs-site/src/content/docs/zh-cn/reference/configuration/providers.md b/docs-site/src/content/docs/zh-cn/reference/configuration/providers.md index 1bd910af15d..1c5f27d0d5e 100644 --- a/docs-site/src/content/docs/zh-cn/reference/configuration/providers.md +++ b/docs-site/src/content/docs/zh-cn/reference/configuration/providers.md @@ -167,7 +167,7 @@ selector,而不是分配一个新名称。 | `promptCacheKey?` | `boolean` | 为整个 `openai-chat` 提供商启用 `prompt_cache_key` 转发。适配器只转发收到的密钥,不自行生成,但 Claude Messages 转换会从 `metadata.user_id`,或在没有元数据时从模型/系统/工具组合派生密钥。除非预设声明,否则默认关闭;Mistral 按其[提示缓存契约](https://docs.mistral.ai/studio/conversations/advanced/prompt-caching)启用,显式 `false` 可关闭。仅在上游明确支持时启用,严格网关可能以 HTTP 400 拒绝未知字段。 | | `retryOn429?` | `{ enabled?: boolean; attempts?: number; intervalMs?: number; maxIntervalMs?: number; respectRetryAfter?: boolean }` | 仅限 API-key 提供商(`authMode: "key"`)。未配置时关闭,但规范端点的 OpenCode Go 和 Command Code,以及规范 Mistral 的 `openai-chat` 例外:默认重放 6 次、间隔 10 秒、上限 60 秒。配置对象即启用;`enabled: false` 也会关闭该默认策略。流开始前的 HTTP 429 按 `Retry-After` 或固定间隔等待,然后在密钥故障转移之前用同一密钥重放同一请求。覆盖文本轮次恢复、Responses passthrough、图像/视频桥、web-search 侧车和终结续接;自定义 `runTurn` 不在范围内。`attempts` 统计首个 429 后的同密钥重放次数(总发送次数 = `attempts` + 1),预算由整个请求共享。耗尽时仅停止同密钥重放,随后按可用目标执行正常密钥故障转移或最终错误处理;密钥认证 passthrough 无故障转移,会原样返回 429。规范 Mistral Chat 的显式上游延迟超过允许等待时间时,会停止同密钥重放并保留原始 `Retry-After`;池中另一密钥仍可能处理请求。其他提供商仍将等待截断至上限。Mistral 按凭据/模型共享重试截止时间;`requestPacing.enabled: false` 或 `retryOn429.respectRetryAfter: false` 可关闭。无重试时间的 429 在确认每分钟预算不足时触发保守的 60 秒恢复等待,随后仅探测一次,成功即恢复正常流量。正常流量不预测下一次 token 成本。策略使用公布的额度,排除已知耗尽的月度额度,不承诺精确重置时间,也不触发历史压缩。Codex 本身不会重试 429。对象默认值:`enabled: true`、`attempts: 3`、`intervalMs: 5000`、`maxIntervalMs: 60000`(单次等待上限,配置最大 600000)、`respectRetryAfter: true`。 | | `transientRetryOn5xx?` | `{ enabled?: boolean; attempts?: number }` | 仅限使用 key 认证的 `openai-chat` 与 `openai-responses` 提供商。`authMode: "forward"` 的提供商(ChatGPT 账号池)从不读取此选项,保持默认重试次数。可选的流开始前上游瞬态状态码(500、502、503、504、520、521、522)重试:未配置时关闭;对象存在即启用,除非 `enabled: false`。覆盖初始 Responses 请求、终结守卫续接、原生 `/v1/chat/completions`,以及 429/账户恢复重新获取。`attempts` 是单个请求允许向上游发送的总次数,包含首次发送(1..10,默认 3);它是与连接重置恢复共享的按请求预算,因此 `3` 表示最多只有三个实际请求到达提供商。等待采用固定 400 毫秒的指数退避,上限为 5 秒,并遵循 `Retry-After`。此选项独立于处理速率限制的 `retryOn429`;流开始后的故障绝不会重放。 每个 Combo 目标都把预先预约的首次发送计入该目标配置的总次数;`attempts: 1` 只发送一次,不会退还预约来增加额度,也不会提高共享上限。发送前的本地拒绝或取消会释放未使用的预约。 | -| `retryOnReset?` | `{ enabled?: boolean; replacements?: number }` | 适用于原生 `openai-responses` 提供商(包含 `authMode: "forward"`),以及收到上游响应头之前的通用 Responses 转换发送路径。可选地替换一次在调用方尚未观察到任何内容时就失败的发送:未配置时关闭;对象存在即启用,除非 `enabled: false`。涵盖两个不确定阶段——响应头到达前连接断开,以及响应头之后 SSE 正文只承载控制事件时断开。canonical ChatGPT 上游 WebSocket 在 create 帧发出之后、任何 Responses 事件到达之前关闭或出错时,也按同样方式处理,其替换发送走 HTTP。只有自包含的请求才会被替换:`store: false`、完整的 `input`、没有 `previous_response_id`/`conversation`/`stream_id`,且只使用由客户端执行的工具。`replacements` 是单个逻辑请求在所有环节和所有组合子请求中可以进行的替换发送次数(1..2,默认 1);它既不是按环节的重试次数,也不是发送预算,因此替换发送仍必须落在该环节已有的发送额度之内。已经产生输出或工具调用的请求,无论此值为何都不会被替换。如果上游已经开始了第一次推理,被替换的推理仍可能计费,因此该选项默认关闭。 通用转换发送路径的首次发送和重建后的发送都支持响应头之前连接重置后的替换,使用同一授权和发送预算。适配器自行管理的传输,以及收到上游响应头之后的转换流故障,均不在此选项范围内。 | +| `retryOnReset?` | `{ enabled?: boolean; replacements?: number }` | 适用于原生 `openai-responses` 提供商(包含 `authMode: "forward"`),以及收到上游响应头之前的通用 Responses 转换发送路径。可选地替换一次在调用方尚未观察到任何内容时就失败的发送:未配置时关闭;对象存在即启用,除非 `enabled: false`。涵盖两个不确定阶段——响应头到达前连接断开,以及响应头之后 SSE 正文只承载控制事件时断开。canonical ChatGPT 上游 WebSocket 在 create 帧发出之后、任何 Responses 事件到达之前关闭或出错时,也按同样方式处理,其替换发送走 HTTP。只有自包含的请求才会被替换:`store: false`、完整的 `input`、没有 `previous_response_id`/`conversation`/`stream_id`,且只使用由客户端执行的工具。若链式轮次的 `previous_response_id` 已由本代理根据自身重放状态展开,该 ID 会在发送前移除,因此该轮次同样符合条件。重置未被替换时,服务日志会记录原因:`ambiguous-no-policy`、`ambiguous-request-not-replayable` 或 `ambiguous-allowance-spent`。`replacements` 是单个逻辑请求在所有环节和所有组合子请求中可以进行的替换发送次数(1..2,默认 1);它既不是按环节的重试次数,也不是发送预算,因此替换发送仍必须落在该环节已有的发送额度之内。已经产生输出或工具调用的请求,无论此值为何都不会被替换。如果上游已经开始了第一次推理,被替换的推理仍可能计费,因此该选项默认关闭。 通用转换发送路径的首次发送和重建后的发送都支持响应头之前连接重置后的替换,使用同一授权和发送预算。适配器自行管理的传输,以及收到上游响应头之后的转换流故障,均不在此选项范围内。 | | `autoToolChoiceOnlyModels?` | `string[]` | `tool_choice` 只接受 `auto` 或 `none` 的模型;强制选择会被降级。 | | `preserveReasoningContentModels?` | `string[]` | 需要在聊天历史中保留先前 assistant `reasoning_content` 的模型。从仪表板保存时会保留已存储的列表(包括 `[]`)。`PATCH /api/providers?name=` 接受数组,或传入 `null` 清除该字段。将提供方改到其他适配器、base URL 或认证模式的保存不会保留该列表(见下文)。 | | `reasoningDetailsModels?` | `string[]` | 以结构化 `reasoning_details` 数组返回思考内容的模型(启用 `reasoning_split` 的 MiniMax M 系列);流式增量为累积快照,按前缀差分处理,保留的推理以 `reasoning_details` 数组而非 `reasoning_content` 字符串回放。 | diff --git a/docs-site/src/content/docs/zh-tw/reference/configuration/providers.md b/docs-site/src/content/docs/zh-tw/reference/configuration/providers.md index eb5a8fde79f..b0161567746 100644 --- a/docs-site/src/content/docs/zh-tw/reference/configuration/providers.md +++ b/docs-site/src/content/docs/zh-tw/reference/configuration/providers.md @@ -139,7 +139,7 @@ ocx models provider openrouter on | `promptCacheKey?` | `boolean` | 為整個 `openai-chat` provider 啟用 `prompt_cache_key` 轉送。adapter 只轉送收到的 key,不自行產生,但 Claude Messages 轉換會從 `metadata.user_id`,或在沒有 metadata 時從模型/系統/工具組合衍生 key。除非 preset 宣告,否則預設關閉;Mistral 依其[提示快取契約](https://docs.mistral.ai/studio/conversations/advanced/prompt-caching)啟用,明確的 `false` 可停用。僅在上游明確支援時啟用,嚴格閘道可能以 HTTP 400 拒絕未知欄位。 | | `retryOn429?` | `{ enabled?: boolean; attempts?: number; intervalMs?: number; maxIntervalMs?: number; respectRetryAfter?: boolean }` | 僅限 API-key provider(`authMode: "key"`)。未設定時關閉,但標準端點的 OpenCode Go 與 Command Code,以及標準 Mistral 的 `openai-chat` 例外:預設 replay 6 次、間隔 10 秒、上限 60 秒。設定物件即啟用;`enabled: false` 也會停用該預設策略。串流開始前的 HTTP 429 會依 `Retry-After` 或固定間隔等待,然後在 key 容錯移轉前以同一 key 重播相同請求。涵蓋文字回合復原、Responses passthrough、圖片/影片橋接、web-search 輔助服務及終端續接;自訂 `runTurn` 不在範圍內。`attempts` 計算第一個 429 後的同 key replay 次數(總傳送次數 = `attempts` + 1),預算由整個請求共用。耗盡時僅停止同 key replay,接著依可用目的地執行一般 key 容錯移轉或最終錯誤處理;金鑰認證 passthrough 沒有容錯移轉,會原樣回傳 429。標準 Mistral Chat 的明確上游延遲超過允許等待時間時,停止同 key replay 並保留原始 `Retry-After`;pool 中另一個 key 仍可能處理請求。其他 provider 仍將等待截斷至上限。Mistral 依 credential/模型共用重試期限;`requestPacing.enabled: false` 或 `retryOn429.respectRetryAfter: false` 可停用。沒有重試時間的 429 在確認每分鐘預算不足時觸發保守的 60 秒復原等待,隨後只探測一次,成功即恢復一般流量。正常流量不預測下一次 token 成本。策略使用公告額度,排除已知耗盡的月額度,不承諾精確重設時間,也不觸發歷史壓縮。Codex 本身不重試 429。物件預設值:`enabled: true`、`attempts: 3`、`intervalMs: 5000`、`maxIntervalMs: 60000`(單次等待上限,設定最大 600000)、`respectRetryAfter: true`。 | | `transientRetryOn5xx?` | `{ enabled?: boolean; attempts?: number }` | 僅限使用金鑰認證的 `openai-chat` 與 `openai-responses` 供應商。`authMode: "forward"` 的供應商(ChatGPT 帳號池)從不讀取此選項,維持預設重試次數。選擇性重試串流開始前的暫時性上游狀態(500、502、503、504、520、521、522):未設定時停用;只要有此物件即啟用,除非 `enabled: false`。涵蓋初始 `Responses` 請求、終止防護續接、原生 `/v1/chat/completions`,以及 429/帳號復原的重新擷取。`attempts` 是單一請求允許傳送至上游的總次數,包含第一次(1..10,預設 3);這是與連線重設復原共用的單一請求範圍預算,因此 `3` 表示最多只有三個實際請求會送達供應商。等待採固定 400 毫秒、上限 5 秒的指數退避,並遵循 `Retry-After`。此機制獨立於處理速率限制的 `retryOn429`;串流中的失敗絕不重播。 每個 Combo 目標都把預先預約的首次傳送計入該目標設定的總次數;`attempts: 1` 只傳送一次,不會退還預約來增加額度,也不會提高共用上限。傳送前的本機拒絕或取消會釋放未使用的預約。 | -| `retryOnReset?` | `{ enabled?: boolean; replacements?: number }` | 適用於原生 `openai-responses` 供應商(包含 `authMode: "forward"`),以及收到上游回應標頭之前的通用 Responses 轉換傳送路徑。可選擇性地替換一次在呼叫端尚未觀察到任何內容時就失敗的傳送:未設定時停用;只要有此物件即啟用,除非 `enabled: false`。涵蓋兩個不確定階段——回應標頭抵達前連線中斷,以及標頭之後 SSE 內文只載有控制事件時中斷。canonical ChatGPT upstream WebSocket 在 create 訊框送出之後、任何 Responses 事件抵達之前關閉或出錯時,也以相同方式處理,其替換傳送改走 HTTP。只有自我完備的請求才會被替換:`store: false`、完整的 `input`、沒有 `previous_response_id`/`conversation`/`stream_id`,且僅使用由用戶端執行的工具。`replacements` 是單一邏輯請求在所有環節與所有組合子請求中可進行的替換傳送次數(1..2,預設 1);它既不是各環節的重試次數,也不是傳送預算,因此替換傳送仍必須落在該環節既有的傳送額度之內。已經產生輸出或工具呼叫的請求,無論此值為何都不會被替換。若上游已經開始第一次推論,被替換的推論仍可能計費,因此此選項預設停用。 通用轉換傳送路徑的首次傳送和重建後的傳送都支援標頭之前連線重設後的替換,使用同一授權和傳送預算。配接器自行管理的傳輸,以及收到上游回應標頭之後的轉換串流故障,均不在此選項範圍內。 | +| `retryOnReset?` | `{ enabled?: boolean; replacements?: number }` | 適用於原生 `openai-responses` 供應商(包含 `authMode: "forward"`),以及收到上游回應標頭之前的通用 Responses 轉換傳送路徑。可選擇性地替換一次在呼叫端尚未觀察到任何內容時就失敗的傳送:未設定時停用;只要有此物件即啟用,除非 `enabled: false`。涵蓋兩個不確定階段——回應標頭抵達前連線中斷,以及標頭之後 SSE 內文只載有控制事件時中斷。canonical ChatGPT upstream WebSocket 在 create 訊框送出之後、任何 Responses 事件抵達之前關閉或出錯時,也以相同方式處理,其替換傳送改走 HTTP。只有自我完備的請求才會被替換:`store: false`、完整的 `input`、沒有 `previous_response_id`/`conversation`/`stream_id`,且僅使用由用戶端執行的工具。若鏈式輪次的 `previous_response_id` 已由本代理依自身重放狀態展開,該 ID 會在傳送前移除,因此該輪次同樣符合條件。重置未被替換時,服務日誌會記錄原因:`ambiguous-no-policy`、`ambiguous-request-not-replayable` 或 `ambiguous-allowance-spent`。`replacements` 是單一邏輯請求在所有環節與所有組合子請求中可進行的替換傳送次數(1..2,預設 1);它既不是各環節的重試次數,也不是傳送預算,因此替換傳送仍必須落在該環節既有的傳送額度之內。已經產生輸出或工具呼叫的請求,無論此值為何都不會被替換。若上游已經開始第一次推論,被替換的推論仍可能計費,因此此選項預設停用。 通用轉換傳送路徑的首次傳送和重建後的傳送都支援標頭之前連線重設後的替換,使用同一授權和傳送預算。配接器自行管理的傳輸,以及收到上游回應標頭之後的轉換串流故障,均不在此選項範圍內。 | | `autoToolChoiceOnlyModels?` | `string[]` | 其 `tool_choice` 僅接受 `auto` 或 `none` 的模型;強制選擇被降級。 | | `preserveReasoningContentModels?` | `string[]` | 需要在 chat 歷史中保留先前 assistant `reasoning_content` 的模型。從儀表板儲存時會保留已儲存的清單(包括 `[]`)。`PATCH /api/providers?name=` 接受陣列,或傳入 `null` 清除該欄位。將供應商改到其他轉接器、base URL 或驗證模式的儲存不會保留該清單(見下文)。 | | `reasoningDetailsModels?` | `string[]` | 以結構化 `reasoning_details` 陣列回傳思考內容的模型(啟用 `reasoning_split` 的 MiniMax M 系列);串流增量為累積快照,以前綴差分處理,保留的推理以 `reasoning_details` 陣列而非 `reasoning_content` 字串重播。 | diff --git a/scripts/test-layout/layout-responses.json b/scripts/test-layout/layout-responses.json index e28aac8d851..9c6415c9223 100644 --- a/scripts/test-layout/layout-responses.json +++ b/scripts/test-layout/layout-responses.json @@ -16,5 +16,7 @@ "responses-workflow-send-ceiling.test.ts": "responses", "responses-websocket-workflow-ceiling.test.ts": "responses", "ws-native-continuation-send-budget.test.ts": "responses", - "responses-native-compact-ocx-envelope.test.ts": "responses" + "responses-native-compact-ocx-envelope.test.ts": "responses", + "responses-reset-replay.test.ts": "responses", + "responses-reset-replay-chained.test.ts": "responses" } diff --git a/scripts/test-layout/layout.json b/scripts/test-layout/layout.json index 9fd7c9a9d0a..436c6c44fba 100644 --- a/scripts/test-layout/layout.json +++ b/scripts/test-layout/layout.json @@ -1582,7 +1582,6 @@ "responses-preview-main-read-fence.test.ts": "responses", "responses-reasoning-effort-downgrade.test.ts": "responses", "responses-native-ultra-effort-wire.test.ts": "responses", "responses-url-citation-title-repair.test.ts": "responses", "responses-reasoning-summary-passthrough.test.ts": "responses", - "responses-reset-replay.test.ts": "responses", "responses-translated-reset.test.ts": "responses", "responses-routed-web-search-fields.test.ts": "responses", "responses-self-named-namespace-scrub.test.ts": "responses", diff --git a/src/server/responses/passthrough-dispatch.ts b/src/server/responses/passthrough-dispatch.ts index 2bb3e78499c..f5142d76fe8 100644 --- a/src/server/responses/passthrough-dispatch.ts +++ b/src/server/responses/passthrough-dispatch.ts @@ -169,8 +169,7 @@ import { } from "./core-opaque-recovery"; import type { RequestLogContext } from "../request-log"; import { deferProtocolSafeResetRecovery, preflightComboStreamResponse } from "./combo-stream-preflight"; -import { authorizeResendForRecovery } from "../../lib/request-resend-gate"; -import { ambiguousResendAllowanceFor, selfContainedResponsesBody } from "./reset-replay"; +import { ambiguousResendAllowanceFor, ambiguousResendAuthorizer, nativePassthroughReplayJudgmentBody, selfContainedResponsesBody } from "./reset-replay"; import { upstreamErrorMessageFromPayload, ENCRYPTED_FUNCTION_OUTPUT_REJECTION } from "../../lib/errors"; import { isTransientConsoleGoUploadRejection } from "../../providers/opencode-zen-rate-limit"; import { planReasoningEffortDowngrade } from "../../providers/reasoning-metadata"; @@ -774,7 +773,7 @@ export async function preparePassthroughExchange( */ let selfContainedJudgment: boolean | undefined; const requestIsSelfContained = (): boolean => - selfContainedJudgment ??= selfContainedResponsesBody(parsed._rawBody); + selfContainedJudgment ??= selfContainedResponsesBody(nativePassthroughReplayJudgmentBody(parsed)); /** * The operator's replacement grant for THIS logical request. * @@ -785,6 +784,7 @@ export async function preparePassthroughExchange( */ const ambiguousResend = () => ambiguousResendAllowanceFor(route.provider, requestIsSelfContained, claimAmbiguousResend); + const authorizeAmbiguousResend = ambiguousResendAuthorizer(ambiguousResend, () => safeHostLabel(request.url)); /** * The pre-header row of the stage table, asked through the one gate. * @@ -792,8 +792,7 @@ export async function preparePassthroughExchange( * the server tree; routing the answer through `authorizeResendForRecovery` here is what * keeps the decision derived from the table rather than restated as a boolean. */ - const claimPreHeaderResend = (): boolean => - authorizeResendForRecovery("pre-header", "connection-reset", ambiguousResend()).allowed; + const claimPreHeaderResend = (): boolean => authorizeAmbiguousResend("pre-header"); /** * The one replacement send the ambiguous rows at the end of the recovery loop may buy: an SSE * body that died before any output, and a Codex WebSocket that died under its create frame @@ -1870,7 +1869,7 @@ export async function preparePassthroughExchange( // Asked before the gate, which claims last: a replacement the budget cannot fund must not // spend the request's one grant. && remainingTransientSendBudget(transientSendAttempts()) > 0 - && authorizeResendForRecovery(socketDeathStage, "connection-reset", ambiguousResend()).allowed + && authorizeAmbiguousResend(socketDeathStage) ) { try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ } console.warn(`[upstream-retry] codex websocket died before any Responses event (${safeHostLabel(request.url)}); ` @@ -1931,7 +1930,7 @@ export async function preparePassthroughExchange( // pre-header helper shares. A committed stage refuses here without touching the // allowance, which is what keeps a turn that already emitted output from draining // the replacement a later ambiguous reset would have been entitled to. - authorize: () => authorizeResendForRecovery(stage, "connection-reset", ambiguousResend()).allowed, + authorize: () => authorizeAmbiguousResend(stage), acceptResponse: candidate => { const type = candidate.headers.get("content-type")?.toLowerCase() ?? ""; return type.includes("text/event-stream") || (!type && upstreamRequestsStream); diff --git a/src/server/responses/reset-replay.ts b/src/server/responses/reset-replay.ts index 00c3f7a963d..95edc4207b1 100644 --- a/src/server/responses/reset-replay.ts +++ b/src/server/responses/reset-replay.ts @@ -14,10 +14,15 @@ * hosted tools into client execution, so every hazard that reaches the wire was visible here, * and a hazard visible here may already have been removed. A cheap fail-closed answer beats * re-parsing a multi-megabyte outbound body on every send. + * + * One removal is certain enough to judge on directly: a `previous_response_id` this proxy + * expanded from its own replay state. The native adapter strips that id from every send, so the + * wire carries the complete input and no continuation handle (#8453). */ -import type { OcxProviderConfig } from "../../types"; +import type { OcxParsedRequest, OcxProviderConfig } from "../../types"; import { resetReplayPolicyFor } from "../../providers/key-failover"; -import type { AmbiguousResendAllowance } from "../../lib/request-resend-gate"; +import { authorizeResendForRecovery, type AmbiguousResendAllowance, type ResendDecision } from "../../lib/request-resend-gate"; +import type { RequestFailureStage } from "../../lib/request-failure-model"; /** Input items a client owns end to end: replaying them re-runs nothing but the model. */ const CLIENT_INPUT_ITEM_TYPES: ReadonlySet = new Set([ @@ -78,6 +83,57 @@ export function selfContainedResponsesBody(body: unknown): boolean { }); } +/** + * The native passthrough body to judge: the inbound body, minus a `previous_response_id` the + * proxy already expanded into `input`. + * + * `expandPreviousResponseInput` keeps the id on the expanded body because Kiro and Cursor read + * their conversation ids from it, so the inbound body always names it. The native Responses + * adapter (and the Azure adapter that wraps it) removes it whenever `_previousResponseInputExpanded` + * is set, which is the flag read here. Judging the id that never reaches the wire refused every + * chained Codex turn, so an operator's `retryOnReset` opt-in applied only to first turns. + * + * An id that was NOT expanded is forwarded to the destination and still refuses: that send + * continues state stored on the origin. + */ +export function nativePassthroughReplayJudgmentBody( + parsed: Pick, +): unknown { + const body = parsed._rawBody; + if (parsed._previousResponseInputExpanded !== true || !record(body) || !Object.hasOwn(body, "previous_response_id")) { + return body; + } + const { previous_response_id: _expandedLocally, ...wire } = body; + return wire; +} + +/** + * Log why an ambiguous reset was not replaced. Operators who opted in otherwise saw only the + * terminal 429 and could not tell a missing policy from a request judged not replayable or an + * allowance already spent (#8453). Host label, stage and reason only: no body, no credential. + */ +export function warnAmbiguousResendRefusal(decision: ResendDecision, hostLabel: string): void { + if (decision.allowed || !decision.refusal.startsWith("ambiguous-")) return; + console.warn(`[upstream-retry] ambiguous reset not replaced (${hostLabel}, ${decision.stage}): ${decision.refusal}`); +} + +/** + * One gate answer for every ambiguous row of a lane -- pre-header, post-header SSE, and a Codex + * WebSocket that died under its create frame -- asked through `authorizeResendForRecovery` and + * with its refusal reason logged. The allowance and host label are read per ask, because the + * recovery loop reassigns the provider row and the request URL. + */ +export function ambiguousResendAuthorizer( + allowance: () => AmbiguousResendAllowance | undefined, + hostLabel: () => string, +): (stage: RequestFailureStage) => boolean { + return stage => { + const decision = authorizeResendForRecovery(stage, "connection-reset", allowance()); + warnAmbiguousResendRefusal(decision, hostLabel()); + return decision.allowed; + }; +} + /** * Chat Completions tools are executed by the client by spec -- the origin only emits the call. * Anything else (`web_search`, `code_interpreter`, a vendor hosted tool) runs on the origin during diff --git a/structure/transports/responses-failover.md b/structure/transports/responses-failover.md index ebc282f1d0d..b4463758cfd 100644 --- a/structure/transports/responses-failover.md +++ b/structure/transports/responses-failover.md @@ -82,6 +82,7 @@ request's execution budget, so a combo child that derives its own scope draws on counter rather than holding a second. A replacement never widens a send budget: it still has to fit inside the allowance the leg already had, and it is charged to the same counter every other send goes through; initial Combo bookings follow the [prepaid-send contract](responses-spend.md#prepaid-initial-sends). +Native passthrough judges the inbound body minus a `previous_response_id` this proxy already expanded from its own replay state, because the native adapter strips that id from every send; an unexpanded id still refuses. Every ambiguous refusal on that lane is logged with its stage and reason (`ambiguous-no-policy`, `ambiguous-request-not-replayable`, `ambiguous-allowance-spent`) and nothing from the body. Coverage: `tests/responses/responses-reset-replay-chained.test.ts` (#8453). Generic translated dispatch in `src/server/responses/adapter-dispatch.ts` asks the same pre-header gate for initial and rebuilt sends, sharing the replacement grant and charging each physical send once to the existing request/workflow budgets. Adapter-owned transports and translated post-header failures are excluded. Coverage: `tests/responses/responses-translated-reset.test.ts`. The number of replacements is the request's as well. A leg reads it from `route.provider`, which credential rotation, OAuth refresh, transport resolution and each combo target reassign inside one diff --git a/tests/fixtures/test-layout-expected.json b/tests/fixtures/test-layout-expected.json index 25ba4fd2811..939358ac007 100644 --- a/tests/fixtures/test-layout-expected.json +++ b/tests/fixtures/test-layout-expected.json @@ -1310,7 +1310,7 @@ "responses-parser.test.ts": "responses", "responses-passthrough-transient-policy.test.ts": "responses", "responses-pool-401-refresh.test.ts": "responses", "responses-pool-refresh-attribution.test.ts": "responses", "responses-preview-main-read-fence.test.ts": "responses", "responses-reasoning-effort-downgrade.test.ts": "responses", "responses-native-ultra-effort-wire.test.ts": "responses", - "responses-url-citation-title-repair.test.ts": "responses", "responses-reasoning-summary-passthrough.test.ts": "responses", "responses-reset-replay.test.ts": "responses", + "responses-url-citation-title-repair.test.ts": "responses", "responses-reasoning-summary-passthrough.test.ts": "responses", "responses-reset-replay.test.ts": "responses", "responses-reset-replay-chained.test.ts": "responses", "responses-translated-reset.test.ts": "responses", "responses-routed-web-search-fields.test.ts": "responses", "responses-self-named-namespace-scrub.test.ts": "responses", "responses-send-budget-counts.test.ts": "responses", "responses-send-budget-errors.test.ts": "responses", "responses-memory-models.test.ts": "responses", diff --git a/tests/responses/responses-reset-replay-chained.test.ts b/tests/responses/responses-reset-replay-chained.test.ts new file mode 100644 index 00000000000..7576e8f1a80 --- /dev/null +++ b/tests/responses/responses-reset-replay-chained.test.ts @@ -0,0 +1,101 @@ +import { afterAll, afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test"; +import type { OcxConfig, OcxProviderConfig } from "../../src/types"; +import { acquireOwnedSpendHome } from "../helpers/owned-spend-home"; + +// #8453: a Codex turn chained with previous_response_id is expanded locally, and the native +// adapter strips the id before the wire. The ambiguous-reset judgment has to look at that body, +// not at the inbound one that still names the id, or an operator's retryOnReset opt-in never +// applies to a chained turn. +const { handleResponses } = await import("../../src/server/responses"); + +const releaseSpendHome = acquireOwnedSpendHome(); +afterAll(() => releaseSpendHome()); + +let warnings: string[] = []; +let warnSpy: ReturnType | undefined; +beforeEach(() => { + warnings = []; + warnSpy = spyOn(console, "warn").mockImplementation((...args: unknown[]) => { warnings.push(args.map(String).join(" ")); }); +}); +afterEach(() => warnSpy?.mockRestore()); + +const completed = (id: string) => ({ + id, object: "response", status: "completed", + output: [{ id: `msg_${id}`, type: "message", role: "assistant", status: "completed", + content: [{ type: "output_text", text: "ok", annotations: [] }] }], + usage: { input_tokens: 3, output_tokens: 2 }, +}); +const sse = (id: string) => new Response( + [{ type: "response.created", response: { id, status: "in_progress" } }, { type: "response.completed", response: completed(id) }] + .map(event => `data: ${JSON.stringify(event)}\n\n`).join(""), + { headers: { "content-type": "text/event-stream" } }, +); +const reset = () => Object.assign(new Error("The socket connection was closed unexpectedly."), { code: "ECONNRESET" }); +const userTurn = (text: string) => [{ type: "message", role: "user", content: [{ type: "input_text", text }] }]; +const request = (body: Record) => new Request("http://localhost/v1/responses", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ model: "fixture/model", stream: true, store: false, ...body }), +}); + +function fixture(prefix: string, extra: Partial) { + const sends: Record[] = []; + const plan: ("reset" | "answer")[] = []; + const provider: OcxProviderConfig & { fetch?: typeof globalThis.fetch } = { + adapter: "openai-responses", baseUrl: "https://gateway.example.test/v1", apiKey: "fixture-key", models: ["model"], ...extra, + fetch: async (_input, init) => { + sends.push(JSON.parse(String(init?.body)) as Record); + if (plan.shift() === "reset") throw reset(); + return sse(`${prefix}_${sends.length}`); + }, + }; + const cfg = { port: 0, defaultProvider: "fixture", providers: { fixture: provider } } satisfies OcxConfig; + return { cfg, sends, plan }; +} + +describe("ambiguous reset on a chained native Responses turn", () => { + test("a locally expanded previous_response_id does not block an opted-in replacement", async () => { + const f = fixture("resp_chain_a", { retryOnReset: { enabled: true, replacements: 2 } }); + const first = await handleResponses(request({ input: userTurn("one") }), f.cfg, { model: "", provider: "" }); + expect(first.status).toBe(200); + await first.text(); + + f.plan.push("reset", "answer"); + const chained = await handleResponses( + request({ previous_response_id: "resp_chain_a_1", input: userTurn("two") }), f.cfg, { model: "", provider: "" }); + expect(chained.status).toBe(200); + expect(await chained.text()).toContain('"type":"response.completed"'); + // Turn one, the send that reset, and the one replacement. + expect(f.sends).toHaveLength(3); + for (const sent of f.sends.slice(1)) { + expect(Object.hasOwn(sent, "previous_response_id")).toBe(false); + expect((sent.input as unknown[]).length).toBe(3); + } + }); + + test("an unexpanded previous_response_id still reaches the wire and stays refused", async () => { + const f = fixture("resp_chain_b", { retryOnReset: { enabled: true, replacements: 2 } }); + f.plan.push("reset", "answer"); + const chained = await handleResponses( + request({ previous_response_id: "resp_never_recorded", input: userTurn("two") }), f.cfg, { model: "", provider: "" }); + expect(chained.status).toBe(429); + expect(await chained.text()).toContain("upstream_reset_replay_refused"); + expect(f.sends).toHaveLength(1); + expect(f.sends[0]!.previous_response_id).toBe("resp_never_recorded"); + expect(warnings.some(line => line.includes("ambiguous-request-not-replayable"))).toBe(true); + }); + + test("the refusal names a missing opt-in without echoing the request", async () => { + const f = fixture("resp_chain_c", {}); + f.plan.push("reset", "answer"); + const response = await handleResponses(request({ input: userTurn("private words") }), f.cfg, { model: "", provider: "" }); + expect(response.status).toBe(429); + await response.text(); + expect(f.sends).toHaveLength(1); + const refusal = warnings.find(line => line.includes("ambiguous-no-policy")); + expect(refusal).toContain("gateway.example.test"); + expect(refusal).not.toContain("private words"); + expect(refusal).not.toContain("fixture-key"); + }); +}); + From ff7c95bcd7aae3dd1d4cc50b57f25bc8bb8af1e9 Mon Sep 17 00:00:00 2001 From: JUN Date: Wed, 11 Nov 2026 03:20:27 +0900 Subject: [PATCH 2/2] test(layout): keep responses-reset-replay in layout.json now that dev is under its cap --- scripts/test-layout/layout-responses.json | 1 - scripts/test-layout/layout.json | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/test-layout/layout-responses.json b/scripts/test-layout/layout-responses.json index 9c6415c9223..05026738d1e 100644 --- a/scripts/test-layout/layout-responses.json +++ b/scripts/test-layout/layout-responses.json @@ -17,6 +17,5 @@ "responses-websocket-workflow-ceiling.test.ts": "responses", "ws-native-continuation-send-budget.test.ts": "responses", "responses-native-compact-ocx-envelope.test.ts": "responses", - "responses-reset-replay.test.ts": "responses", "responses-reset-replay-chained.test.ts": "responses" } diff --git a/scripts/test-layout/layout.json b/scripts/test-layout/layout.json index 81ea658800f..bcb8d32afe0 100644 --- a/scripts/test-layout/layout.json +++ b/scripts/test-layout/layout.json @@ -1581,6 +1581,7 @@ "responses-preview-main-read-fence.test.ts": "responses", "responses-reasoning-effort-downgrade.test.ts": "responses", "responses-native-ultra-effort-wire.test.ts": "responses", "responses-url-citation-title-repair.test.ts": "responses", "responses-reasoning-summary-passthrough.test.ts": "responses", + "responses-reset-replay.test.ts": "responses", "responses-translated-reset.test.ts": "responses", "responses-routed-web-search-fields.test.ts": "responses", "responses-self-named-namespace-scrub.test.ts": "responses",