diff --git a/docs-site/src/content/docs/fr/reference/adapters.md b/docs-site/src/content/docs/fr/reference/adapters.md index d45e12b48a1..2ac70dc2091 100644 --- a/docs-site/src/content/docs/fr/reference/adapters.md +++ b/docs-site/src/content/docs/fr/reference/adapters.md @@ -124,6 +124,8 @@ Les comptes inactifs n’ont pas encore nécessairement de liste observée. - Construit le `conversationState` de Kiro, mappe les outils Codex et leurs résultats, puis envoie les blocs d’image pris en charge par le protocole Kiro. - Décode `application/vnd.amazon.eventstream`, reconstruit les événements de texte, de raisonnement et d’outil, détecte les données JSON d’outil tronquées et estime l’utilisation, car le service en amont ne renvoie aucun nombre de jetons. Les jetons restent estimés, mais les crédits de `meteringEvent` sont mesurés dans `providerCredits` : la dernière valeur d'une réponse est retenue, puis les envois physiques facturés séparément sont additionnés. Aucun crédit n'est déduit des jetons. +- Envoie au plus 48 outils client et 96 Ko de JSON d’outil converti par requête, et coupe chaque description d’outil à 1 024 caractères (9 216 pour `gpt-5.6-sol`). Les outils chargés par tool search et l’outil d’exécution du code mode Codex passent en premier ; le modèle est informé des outils omis. Réglez `OCX_KIRO_TOOL_BUDGET=off` dans l’environnement du proxy pour envoyer tout le catalogue client, et `OCX_KIRO_TOOL_DESCRIPTION_LIMIT=off` pour envoyer les descriptions complètes. Les deux sont lus à la conversion de la requête, s’appliquent à tous les modèles Kiro et laissent les autres limites intactes. Désactiver le budget supprime la limite elle-même : il n’y a plus rien à prioriser ni à omettre. Tous les outils déclarés partent dans l’ordre de déclaration, l’avis d’omission disparaît, et un client qui en déclare plus que Kiro n’en accepte reçoit le `ValidationException` amont au lieu d’un catalogue réduit. La taille des requêtes suit alors le catalogue du client. Ces interrupteurs sont une option de retrait pour un compte mesuré comme acceptant la requête plus grande, pas un moyen de rendre un catalogue surdimensionné sûr. +- Retire les mots-clés JSON Schema que le validateur de Kiro rejette (`pattern`, `minLength`, `maximum`, `additionalProperties` et 25 autres) des schémas d’outil et de sortie structurée, sauf ceux qu’une sonde a mesurés sur le modèle concerné. `claude-opus-4.5` les conserve tous. `claude-opus-5.5` et `claude-sonnet-5.5` conservent `pattern` et `maximum` ; leurs autres mots-clés restent retirés, faute de sonde qui les leur ait envoyés. Tout autre modèle Kiro, y compris les autres identifiants Claude, garde le retrait complet. L’exception est par modèle et par mot-clé parce que ce protocole sépare déjà les modèles Claude sur l’effort de raisonnement, et un mot-clé rejeté par Kiro fait échouer la requête entière au lieu de seulement priver le modèle d’une indication. - Utilise à l’identique le `baseUrl` configuré lorsqu’il est personnalisé. Une URL canonique `runtime.{region}.kiro.dev` suit la région d’API de l’identifiant importé ; seule cette forme canonique peut faire l’objet d’un unique repli borné vers `q.{region}.amazonaws.com` après un échec de point de terminaison, de signature, de DNS ou de connexion, ou une réponse HTTP 502/503/504 reçue avant toute sortie. - Gère la récupération après réinitialisation de connexion lorsqu’un rejeu est sûr, cet unique repli de point de terminaison admissible, une actualisation OAuth suivie d’un rejeu après une réponse HTTP 401, ainsi qu’une récupération bornée pour les réponses Kiro 429 transitoires. Un délai de récupération partagé et une seule sonde après ce délai empêchent les requêtes concurrentes d’épuiser des budgets de nouvelle tentative indépendants ; un quota épuisé n’est pas réessayé sur le même compte, et les autres erreurs de service ne sont pas rejouées. Tous les envois Kiro utilisent la sortie réseau configurée ; un délai d’en-tête dépassé renvoie 504, l’annulation du client arrête la requête et les erreurs HTTP 5xx finales affichent un texte public fixe. - Avec deux comptes enregistrés, un refus de débit refroidit brièvement le compte concerné. Un refus mensuel confirmé (HTTP 400 ou 429) l’exclut jusqu’à la réinitialisation observée ou l’expiration des données ; une suspension confirmée (HTTP 403) le met temporairement en quarantaine, mais un 403 ordinaire ne déclenche pas de rotation. La rotation après refus reste active lorsque la préférence proactive est désactivée. Le choix d’un autre compte avant le premier envoi exige une préférence proactive, où le réglage du fournisseur prime sur le réglage global. Un tour terminé par le même compte efface un ancien verdict d’épuisement. @@ -141,12 +143,43 @@ Si Kiro s’arrête sans appeler l’outil d’achèvement, l’adaptateur effec ### Effort de raisonnement -Les modèles GPT-5.6 utilisent `additionalModelRequestFields.reasoning.effort`, et `claude-opus-5` -utilise `additionalModelRequestFields.output_config.effort`. Pour `gpt-5.6-luna` et `gpt-5.6-terra`, -seuls `low`, `medium`, `high` et `max` empruntent le chemin natif vérifié. Leur niveau `xhigh` -conserve les instructions de réflexion bornées existantes, car ce niveau natif n’a pas été vérifié. -`gpt-5.6-sol` et `claude-opus-5` conservent leurs niveaux natifs existants : `low`, `medium`, `high`, -`xhigh` et `max`. Les autres modèles Kiro utilisent une émulation ; un réglage d’effort ne prouve pas une prise en charge native. +Les modèles GPT-5.6 et GPT-6 (`gpt-6-sol`, `gpt-6-luna`, `gpt-6.1-sol`, `gpt-6.1-terra`) +utilisent `additionalModelRequestFields.reasoning.effort`, et les modèles Claude listés ci-dessous +utilisent `additionalModelRequestFields.output_config.effort`. +Pour `gpt-5.6-luna` et `gpt-5.6-terra`, seuls `low`, `medium`, `high` et `max` empruntent le +chemin natif vérifié. Leur niveau `xhigh` conserve les instructions de réflexion bornées +existantes, car ce niveau natif n’a pas été vérifié. +`gpt-5.6-sol` et les modèles GPT-6 conservent leurs niveaux natifs `low`, `medium`, `high`, +`xhigh` et `max` par ce même champ `reasoning.effort`. + +Kiro accepte `output_config.effort` pour `claude-opus-5`, `claude-opus-5.5`, `claude-opus-4.8`, +`claude-opus-4.7`, `claude-opus-4.6`, `claude-sonnet-5.5`, `claude-sonnet-5`, `claude-sonnet-4.6` +et `claude-haiku-5.5` aux niveaux `low`, `medium`, `high`, `xhigh` et `max`, avec une +exception : `claude-opus-4.6` et `claude-sonnet-4.6` rejettent le `xhigh` natif, donc ce niveau +garde le bloc de réflexion émulé. + +Kiro n’a pas de niveau `minimal`. Sur les neuf modèles ci-dessus sauf `claude-opus-5` et +`claude-opus-5.5`, `minimal` conserve le bloc émulé au lieu de faire échouer une requête qui +fonctionnait avant l’activation de l’effort natif. Ces deux modèles Opus 5, ainsi que les modèles GPT +qui portent un effort natif, rejettent `minimal` par une erreur 400 `invalid_request_error` avant +que quoi que ce soit ne soit envoyé. + +Les surfaces rapportent ce refus différemment. Une requête Responses place dans `error.message` une +phrase qui nomme le modèle et le niveau refusé. Les chemins Messages et Chat Completions y placent le +texte fixe du proxy, `The provider rejected the request.`, parce qu’ils remplacent le détail de +l’adaptateur par leur propre libellé au lieu de le relayer. Seul Responses nomme le modèle et le +niveau. Les requêtes en flux et hors flux se lisent à l’identique sur toutes les surfaces : le refus +survient pendant la construction du corps envoyé en amont, donc aucun flux ne s’ouvre et aucun appel +amont n’a lieu. + +Le `ultra` envoyé par l’appelant est réduit à `max` à l’analyse. Les pins `pinnedReasoningEffort`, +`modelPinnedReasoningEfforts` et `modelPinnedEfforts` sont appliqués ensuite : pour les modèles à +effort natif Kiro, l’adaptateur réduit aussi `ultra` à `max`. Pour les modèles en émulation, ce pin +est ignoré et la requête part sans signal de raisonnement. + +`claude-haiku-4.5`, `claude-opus-4.5`, `claude-sonnet-4.5`, ainsi que tout modèle Kiro non +listé ci-dessus, utilisent une émulation à chaque niveau ; un réglage d’effort ne prouve pas une +prise en charge native. ## `cursor` diff --git a/docs-site/src/content/docs/ja/reference/adapters.md b/docs-site/src/content/docs/ja/reference/adapters.md index 08cd6d05489..391632d3cac 100644 --- a/docs-site/src/content/docs/ja/reference/adapters.md +++ b/docs-site/src/content/docs/ja/reference/adapters.md @@ -136,6 +136,25 @@ HTTP リトライ ループの対象外です。 block を送ります。 - `application/vnd.amazon.eventstream` をデコードして text/thinking/tool イベントを復元し、途切れたツール JSON を検出します。上流がトークン数を返さないため使用量は推定します。 - `fetchResponse` で限られた回数だけリトライし、エラーを分類/マスクします。非ストリーミングパーサーはウェブ検索ループのために同じイベントストリームを最後まで消費します。 +- リクエストごとにクライアントツールを最大 48 個、変換後のツール JSON を 96 KB まで送り、ツールの説明は + 1,024 文字(`gpt-5.6-sol` は 9,216 文字)で切り詰めます。tool search で読み込んだツールと Codex + code-mode の実行ツールを先に採用し、省略したツールはモデルに伝えます。プロキシの環境に + `OCX_KIRO_TOOL_BUDGET=off` を設定するとクライアントのカタログ全体を、 + `OCX_KIRO_TOOL_DESCRIPTION_LIMIT=off` を設定すると説明の全文を送ります。どちらもリクエスト変換時に + 読み取られ、すべての Kiro モデルに適用され、他の上限は変わりません。予算を切ると上限自体がなくなり、 + 優先順位も省略も対象がなくなります。宣言したツールがすべて宣言順に送られ、省略の通知も消え、 + Kiro が受け付ける数より多く宣言したクライアントは、切り詰めたカタログではなく上流の + `ValidationException` を受け取ります。リクエストサイズもクライアントのカタログに比例して大きくなります。 + このスイッチは、より大きなリクエストを受け付けると測定されたアカウント向けのオプトアウトであり、 + 過大なカタログを安全にする手段ではありません。 +- Kiro のバリデータが拒否する JSON Schema キーワード(`pattern`、`minLength`、`maximum`、 + `additionalProperties` など 29 個)をツールと構造化出力のスキーマから取り除きます。ただし、 + プローブでそのモデルが受け付けたと確認できたキーワードは残します。`claude-opus-4.5` はすべて残ります。 + `claude-opus-5.5` と `claude-sonnet-5.5` は `pattern`、`maximum` だけが残り、それ以外は取り除かれたままです。この 2 つの + モデルに他のキーワードを送ったプローブがないためです。他の Claude id を含むその他の Kiro モデルは + すべて取り除きます。例外をモデル単位・キーワード単位にしているのは、この wire が reasoning effort で + すでに Claude モデルを分けているためであり、Kiro が拒否するキーワードはヒントを失うだけで済まず、 + リクエスト全体を失敗させるからです。 ### 完了とネイティブ stop reason Kiro のアシスタントテキストには、それ自体で end-turn を示す信頼できる区別がありません。ただし終端の @@ -155,12 +174,39 @@ filtered incomplete になります。実際のツール呼び出しを伴わな ### Reasoning effort -GPT-5.6 系は `additionalModelRequestFields.reasoning.effort`、`claude-opus-5` は -`additionalModelRequestFields.output_config.effort` を使用します。`gpt-5.6-luna` と -`gpt-5.6-terra` では、検証済みの `low`、`medium`、`high`、`max` だけをネイティブフィールドで送信します。 +GPT-5.6 系と GPT-6 系(`gpt-6-sol`、`gpt-6-luna`、`gpt-6.1-sol`、`gpt-6.1-terra`)は +`additionalModelRequestFields.reasoning.effort`、以下に挙げる Claude のモデルは +`additionalModelRequestFields.output_config.effort` を使用します。 +`gpt-5.6-luna` と `gpt-5.6-terra` では、検証済みの `low`、`medium`、`high`、`max` だけをネイティブフィールドで送信します。 両モデルの `xhigh` は未検証のため、従来の上限付き thinking 指示によるエミュレーションを維持します。 -`gpt-5.6-sol` と `claude-opus-5` の既存のネイティブ段階(`low`、`medium`、`high`、`xhigh`、`max`)は変更しません。 -その他の Kiro モデルはエミュレーションを使用し、effort の選択肢だけではネイティブ対応を意味しません。 +`gpt-5.6-sol` と GPT-6 系は、同じ `reasoning.effort` フィールドでネイティブの `low`、`medium`、 +`high`、`xhigh`、`max` を維持します。 + +Kiro は `claude-opus-5`、`claude-opus-5.5`、`claude-opus-4.8`、`claude-opus-4.7`、 +`claude-opus-4.6`、`claude-sonnet-5.5`、`claude-sonnet-5`、`claude-sonnet-4.6`、 +`claude-haiku-5.5` で `low`、`medium`、`high`、`xhigh`、`max` を `output_config.effort` として +受け付けます。例外が 1 つあります。`claude-opus-4.6` と `claude-sonnet-4.6` はネイティブの `xhigh` を +拒否するため、その段階だけエミュレーションの thinking ブロックを使います。 + +Kiro には `minimal` の段階がありません。`claude-opus-5` と `claude-opus-5.5` を除く上記の 9 モデルは、 +ネイティブ effort を有効にする前から動作していたリクエストを失敗させないよう、`minimal` でエミュレーションの +ブロックを維持します。この 2 つの Opus 5 モデルとネイティブ effort を持つ GPT 系は、何も送信する前に +`minimal` を 400 `invalid_request_error` で拒否します。 + +拒否の伝え方はサーフェスごとに異なります。Responses リクエストはモデルと拒否された effort を示す文を +`error.message` に入れます。Messages と Chat Completions の経路はその位置にプロキシの固定文言 +`The provider rejected the request.` を入れます。どちらもアダプターの詳細文をそのまま中継せず、自前の +文言に置き換えるためです。モデルと effort を示すのは Responses だけです。ストリーミングと非ストリーミングは +どのサーフェスでも同じ応答になります。拒否は上流へ送る本文を組み立てる途中で起きるため、ストリームは +開かれず上流呼び出しも発生しません。 + +呼び出し元の `ultra` は解析時に `max` へ変換されます。`pinnedReasoningEffort`、 +`modelPinnedReasoningEfforts`、`modelPinnedEfforts` はその後に適用されます。ネイティブ Kiro +effort を持つモデルでは、アダプターも pin の `ultra` を `max` に変換します。エミュレーション +モデルでは pin は破棄され、推論シグナルなしでリクエストが送られます。 + +`claude-haiku-4.5`、`claude-opus-4.5`、`claude-sonnet-4.5` と、上記にない Kiro モデルは +すべての effort でエミュレーションを使います。effort の選択肢だけではネイティブ対応を意味しません。 ## `cursor` diff --git a/docs-site/src/content/docs/ko/reference/adapters.md b/docs-site/src/content/docs/ko/reference/adapters.md index cd6ed21d0b4..56506f571f5 100644 --- a/docs-site/src/content/docs/ko/reference/adapters.md +++ b/docs-site/src/content/docs/ko/reference/adapters.md @@ -169,6 +169,24 @@ provider를 추가했다고 자격증명이나 권한의 유효성이 검증되 멈춰도 읽기를 중단한 소비자가 디코더 종료를 계속 기다리지 않습니다. - `fetchResponse`에서 제한된 횟수만 재시도하고 오류를 분류/마스킹합니다. 비스트리밍 파서는 웹 검색 루프를 위해 같은 이벤트 스트림을 끝까지 소비합니다. +- 요청마다 클라이언트 툴을 최대 48개, 변환된 툴 JSON을 96 KB까지 보내고, 툴 설명은 1,024자 + (`gpt-5.6-sol`은 9,216자)로 자릅니다. tool search로 불러온 툴과 Codex code-mode 실행 툴을 + 먼저 넣고, 생략된 툴은 모델에 알립니다. 프록시 환경에 `OCX_KIRO_TOOL_BUDGET=off`를 두면 + 클라이언트 카탈로그 전체를, `OCX_KIRO_TOOL_DESCRIPTION_LIMIT=off`를 두면 설명 전체를 + 보냅니다. 둘 다 요청 변환 시점에 읽고 모든 Kiro 모델에 적용되며 다른 한도는 그대로 둡니다. + 예산을 끄면 한도 자체가 없어져 우선순위를 매기거나 생략할 대상이 없습니다. 선언한 툴이 모두 + 선언 순서대로 전송되고 생략 안내도 사라지며, Kiro가 받아들이는 수보다 많이 선언한 + 클라이언트는 잘린 카탈로그 대신 업스트림 `ValidationException`을 받습니다. 요청 크기도 + 클라이언트 카탈로그를 따라 커집니다. 이 스위치는 더 큰 요청을 받아들이는 계정을 위한 + 선택 해제이며, 과대한 카탈로그를 안전하게 만드는 수단이 아닙니다. +- Kiro 검증기가 거부하는 JSON Schema 키워드(`pattern`, `minLength`, `maximum`, + `additionalProperties` 등 29개)를 툴과 구조화 출력 스키마에서 제거하되, 프로브가 해당 + 모델에서 통과를 확인한 키워드는 남깁니다. `claude-opus-4.5`는 전부 남습니다. + `claude-opus-5.5`와 `claude-sonnet-5.5`는 `pattern`, `maximum`만 남고 나머지는 제거된 상태로 남습니다. 그 두 모델에 + 다른 키워드를 보낸 프로브가 없기 때문입니다. 나머지 Claude id를 포함한 다른 Kiro 모델은 + 전부 제거합니다. 예외를 모델별·키워드별로 두는 이유는 이 wire가 이미 reasoning effort에서 + Claude 모델을 나누기 때문이며, Kiro가 거부하는 키워드는 힌트를 잃는 데 그치지 않고 요청 + 전체를 실패시킵니다. ### 완료와 네이티브 stop reason Kiro의 어시스턴트 텍스트에는 그 자체로 턴 종료를 알리는 신뢰할 만한 구분이 없습니다. 다만 종단 @@ -188,12 +206,39 @@ commentary로 유지하고 비공개 완료 툴을 한 번 검증합니다. ### Reasoning effort -GPT-5.6 계열은 `additionalModelRequestFields.reasoning.effort`를, `claude-opus-5`는 -`additionalModelRequestFields.output_config.effort`를 사용합니다. `gpt-5.6-luna`와 -`gpt-5.6-terra`는 검증된 `low`, `medium`, `high`, `max`만 네이티브 필드로 전송합니다. +GPT-5.6 계열과 GPT-6 계열(`gpt-6-sol`, `gpt-6-luna`, `gpt-6.1-sol`, `gpt-6.1-terra`)은 +`additionalModelRequestFields.reasoning.effort`를, 아래에 나열한 Claude 모델은 +`additionalModelRequestFields.output_config.effort`를 사용합니다. +`gpt-5.6-luna`와 `gpt-5.6-terra`는 검증된 `low`, `medium`, `high`, `max`만 네이티브 필드로 전송합니다. 두 모델의 `xhigh`는 네이티브 동작이 검증되지 않아 기존의 제한된 thinking 지시문 방식을 유지합니다. -`gpt-5.6-sol`과 `claude-opus-5`의 기존 네이티브 단계(`low`, `medium`, `high`, `xhigh`, `max`)는 -바뀌지 않습니다. 다른 Kiro 모델의 effort는 에뮬레이션이며, 조절 항목이 있다고 네이티브 지원을 뜻하지는 않습니다. +`gpt-5.6-sol`과 GPT-6 계열은 같은 `reasoning.effort` 필드로 네이티브 `low`, `medium`, `high`, +`xhigh`, `max`를 그대로 사용합니다. + +Kiro는 `claude-opus-5`, `claude-opus-5.5`, `claude-opus-4.8`, `claude-opus-4.7`, +`claude-opus-4.6`, `claude-sonnet-5.5`, `claude-sonnet-5`, `claude-sonnet-4.6`, +`claude-haiku-5.5`에 `low`, `medium`, `high`, `xhigh`, `max`를 `output_config.effort`로 +받아들입니다. 예외가 하나 있습니다. `claude-opus-4.6`과 `claude-sonnet-4.6`은 네이티브 `xhigh`를 +거부하므로 그 단계만 에뮬레이션 thinking 블록을 씁니다. + +Kiro에는 `minimal` 단계가 없습니다. `claude-opus-5`와 `claude-opus-5.5`를 뺀 위의 아홉 모델은 +네이티브 effort를 쓰기 전에도 동작하던 요청을 실패시키지 않으려고 `minimal`에서 에뮬레이션 블록을 +유지합니다. 이 두 Opus 5 모델과 네이티브 effort를 쓰는 GPT 모델은 `minimal`을 아무것도 보내기 전에 +400 `invalid_request_error`로 거부합니다. + +거부를 알리는 방식은 표면마다 다릅니다. Responses 요청은 모델과 거부된 effort를 밝히는 문장을 +`error.message`에 담습니다. Messages와 Chat Completions 경로는 그 자리에 프록시의 고정 문구인 +`The provider rejected the request.`를 넣습니다. 두 경로 모두 어댑터의 세부 문장을 그대로 전달하지 +않고 자기 문구로 바꾸기 때문입니다. 모델과 effort를 밝히는 쪽은 Responses뿐입니다. 스트리밍과 +비스트리밍은 모든 표면에서 같은 응답을 냅니다. 거부는 업스트림 요청 본문을 만드는 도중에 일어나므로 +스트림이 열리지 않고 업스트림 호출도 없습니다. + +호출자가 보낸 `ultra`는 파싱할 때 `max`로 바뀝니다. `pinnedReasoningEffort`, +`modelPinnedReasoningEfforts`, `modelPinnedEfforts` pin은 그 뒤에 적용됩니다. 네이티브 Kiro +effort를 쓰는 모델에서는 어댑터가 pin의 `ultra`도 `max`로 바꿉니다. 에뮬레이션 모델에서는 +해당 pin을 버리고 추론 신호 없이 요청을 보냅니다. + +`claude-haiku-4.5`, `claude-opus-4.5`, `claude-sonnet-4.5`와 위에 없는 Kiro 모델은 모든 +effort에서 에뮬레이션을 씁니다. 조절 항목이 있다고 네이티브 지원을 뜻하지는 않습니다. ## `cursor` diff --git a/docs-site/src/content/docs/reference/adapters.md b/docs-site/src/content/docs/reference/adapters.md index b7d038c1854..825d9046ec7 100644 --- a/docs-site/src/content/docs/reference/adapters.md +++ b/docs-site/src/content/docs/reference/adapters.md @@ -577,6 +577,25 @@ Use `liveModels: false` for a static catalog; Settings can switch between the tw - Accepts Responses `text` controls that are not structured output — `text.verbosity` and `text.format: {"type":"text"}` — without forwarding them. Kiro has no wire field for either, so they are ignored rather than rejected. +- Sends at most 48 client tools and 96 KB of converted tool JSON per request, and cuts each tool + description to 1,024 characters (9,216 for `gpt-5.6-sol`). Tools loaded through tool search and the Codex code-mode + execution tool are admitted first; the model is told which tools were omitted. Set + `OCX_KIRO_TOOL_BUDGET=off` in the proxy's environment to send the whole client catalog, and + `OCX_KIRO_TOOL_DESCRIPTION_LIMIT=off` to send full descriptions. Both are read when a request is + converted, apply to every Kiro model, and leave other limits unchanged. Disabling the budget + removes the limit itself, so there is nothing to prioritize or omit: every declared tool goes + on the wire in declaration order, the omission notice disappears, and a client that declares + more than Kiro accepts gets the upstream `ValidationException` instead of a trimmed catalog. + Request sizes then grow with the client's own catalog. These switches are an opt-out for an + account measured to accept the larger request, not a way to make an oversized catalog safe. +- Strips the JSON Schema keywords Kiro's validator rejects (`pattern`, `minLength`, `maximum`, + `additionalProperties` and 25 others) from tool and structured-output schemas, except the + keywords a probe measured a model taking. `claude-opus-4.5` keeps all of them. `claude-opus-5.5` + and `claude-sonnet-5.5` keep `pattern` and `maximum`; their other keywords stay stripped, because no + probe sent them to those models. Every other Kiro + model, including the other Claude ids, keeps the full strip. The exception is per model and per + keyword because this wire already separates Claude models on reasoning effort, and a keyword + Kiro rejects fails the whole request rather than just costing the model a hint. - Emulates structured output (Responses `text.format` of type `json_schema` or `json_object`, and Messages `output_config.format`). The Kiro wire cannot constrain the response shape, so the adapter advertises a private output tool whose input schema is the requested schema and returns @@ -714,13 +733,42 @@ The continuation's instruction tells the model that its ordinary text has not re ### Reasoning effort -The GPT-5.6 family uses `additionalModelRequestFields.reasoning.effort`; `claude-opus-5` -uses `additionalModelRequestFields.output_config.effort`. For `gpt-5.6-luna` and -`gpt-5.6-terra`, only `low`, `medium`, `high`, and `max` use the verified native path. -Their `xhigh` selection retains the previous bounded thinking instructions in user content -because that native rung has not been verified. `gpt-5.6-sol` and `claude-opus-5` keep -their existing native `low`, `medium`, `high`, `xhigh`, and `max` behavior. Other Kiro -models use emulated reasoning; an advertised effort control is not proof of native support. +The GPT-5.6 family and the GPT-6 models (`gpt-6-sol`, `gpt-6-luna`, `gpt-6.1-sol`, +`gpt-6.1-terra`) use `additionalModelRequestFields.reasoning.effort`; the Claude ids listed +below use `additionalModelRequestFields.output_config.effort`. +For `gpt-5.6-luna` and `gpt-5.6-terra`, only `low`, `medium`, `high`, and `max` use the +verified native path. Their `xhigh` selection retains the previous bounded thinking +instructions in user content because that native rung has not been verified. `gpt-5.6-sol` +and the GPT-6 models keep their native `low`, `medium`, `high`, `xhigh`, and `max` +behavior through that same `reasoning.effort` field. + +Kiro accepts `output_config.effort` for `claude-opus-5`, `claude-opus-5.5`, +`claude-opus-4.8`, `claude-opus-4.7`, `claude-opus-4.6`, `claude-sonnet-5.5`, +`claude-sonnet-5`, `claude-sonnet-4.6`, and `claude-haiku-5.5` at `low`, `medium`, +`high`, `xhigh` and `max`, with one exception: `claude-opus-4.6` and `claude-sonnet-4.6` +reject native `xhigh`, so that rung keeps the emulated thinking block. + +Kiro has no `minimal` rung. On all nine models except `claude-opus-5` and `claude-opus-5.5`, +`minimal` keeps the emulated block instead of failing a request that worked before native effort +was enabled. Those two Opus 5 models, and the GPT models that carry native effort, reject +`minimal` with a 400 `invalid_request_error` before anything is sent. + +The surfaces report that refusal differently. A Responses request puts a sentence naming the model +and the rejected effort in `error.message`. The Messages and Chat Completions paths put their fixed +condition copy there instead — `The provider rejected the request.` — because those paths replace an +adapter detail with their own sentence rather than relaying it. Only Responses names the model and +the effort. Stream and non-stream requests read identically on every surface: the refusal is raised +while the wire payload is built, so no stream has opened and no request has gone upstream. + +A caller's `ultra` is reduced to `max` during parsing. The configured pins +`pinnedReasoningEffort`, `modelPinnedReasoningEfforts` and `modelPinnedEfforts` are applied later; +for models with native Kiro effort, the adapter also folds a pinned `ultra` to native `max`. +For models using emulated reasoning, a pinned `ultra` is dropped and the request is sent without +a reasoning signal. + +`claude-haiku-4.5`, `claude-opus-4.5` and `claude-sonnet-4.5`, along with any Kiro +model not listed above, use emulated reasoning at every effort; an advertised effort control is +not proof of native support. ## `cursor` diff --git a/docs-site/src/content/docs/ru/reference/adapters.md b/docs-site/src/content/docs/ru/reference/adapters.md index 1a88fa8a30c..b2deb82426b 100644 --- a/docs-site/src/content/docs/ru/reference/adapters.md +++ b/docs-site/src/content/docs/ru/reference/adapters.md @@ -169,6 +169,27 @@ Kiro (`https://runtime.{region}.kiro.dev/`). - Через `fetchResponse` сам управляет ограниченными повторными попытками и классифицированными ошибками с удалением чувствительных данных; его непотоковый парсер вычитывает тот же поток событий для цикла веб-поиска. +- Отправляет не более 48 клиентских инструментов и 96 КБ преобразованного JSON инструментов за + запрос, а каждое описание инструмента обрезает до 1 024 символов (9 216 для `gpt-5.6-sol`). + Инструменты, загруженные через tool search, и инструмент исполнения code mode в Codex проходят + первыми, а об опущенных инструментах модель уведомляется. Задайте `OCX_KIRO_TOOL_BUDGET=off` в + окружении прокси, чтобы отправлять весь клиентский каталог, и + `OCX_KIRO_TOOL_DESCRIPTION_LIMIT=off` — чтобы отправлять полные описания. Оба значения + читаются при преобразовании запроса, действуют для всех моделей Kiro и не меняют остальные + ограничения. Отключение бюджета убирает само ограничение: сортировать и опускать больше нечего. + Все объявленные инструменты уходят в порядке объявления, уведомление об опущенных исчезает, а + клиент, объявивший больше, чем принимает Kiro, получает вышестоящий `ValidationException` + вместо урезанного каталога. Размер запросов тогда растёт вместе с клиентским каталогом. Эти + переключатели — отказ для аккаунта, измеренного как принимающий более крупный запрос, а не способ + сделать чрезмерный каталог безопасным. +- Удаляет ключевые слова JSON Schema, которые отклоняет валидатор Kiro (`pattern`, `minLength`, + `maximum`, `additionalProperties` и ещё 25), из схем инструментов и структурированного + вывода, кроме тех, что проверка измерила на конкретной модели. `claude-opus-4.5` сохраняет все. + `claude-opus-5.5` и `claude-sonnet-5.5` сохраняют `pattern` и `maximum`; остальные ключевые слова у них по-прежнему удаляются, + потому что проверка их этим моделям не отправляла. Все прочие модели Kiro, включая остальные + идентификаторы Claude, сохраняют полное удаление. Исключение задаётся по модели и по ключевому + слову: этот протокол уже разделяет модели Claude по уровню рассуждений, а отклонённое Kiro + ключевое слово приводит к ошибке всего запроса, а не только к потере подсказки. ### Завершение и нативный stop reason Текст ассистента Kiro сам по себе не даёт надёжного признака конца хода. Однако завершающий @@ -190,12 +211,40 @@ incomplete. `TOOL_USE` без фактического вызова инстру ### Reasoning effort -Семейство GPT-5.6 использует `additionalModelRequestFields.reasoning.effort`, а `claude-opus-5` — -`additionalModelRequestFields.output_config.effort`. Для `gpt-5.6-luna` и `gpt-5.6-terra` нативный -путь проверен только для `low`, `medium`, `high` и `max`. Их `xhigh` сохраняет прежнюю эмуляцию -через ограниченные инструкции thinking, поскольку нативный уровень не проверен. -Существующие нативные уровни `gpt-5.6-sol` и `claude-opus-5` (`low`, `medium`, `high`, `xhigh`, `max`) -не меняются. Остальные модели Kiro используют эмуляцию; наличие настройки effort не доказывает нативную поддержку. +Семейства GPT-5.6 и GPT-6 (`gpt-6-sol`, `gpt-6-luna`, `gpt-6.1-sol`, `gpt-6.1-terra`) используют +`additionalModelRequestFields.reasoning.effort`, а перечисленные ниже модели Claude — +`additionalModelRequestFields.output_config.effort`. Для +`gpt-5.6-luna` и `gpt-5.6-terra` нативный путь проверен только для `low`, `medium`, `high` и `max`. +Их `xhigh` сохраняет прежнюю эмуляцию через ограниченные инструкции thinking, поскольку нативный +уровень не проверен. `gpt-5.6-sol` и модели GPT-6 сохраняют нативные `low`, `medium`, `high`, +`xhigh` и `max` через то же поле `reasoning.effort`. + +Kiro принимает `output_config.effort` для `claude-opus-5`, `claude-opus-5.5`, `claude-opus-4.8`, +`claude-opus-4.7`, `claude-opus-4.6`, `claude-sonnet-5.5`, `claude-sonnet-5`, `claude-sonnet-4.6` +и `claude-haiku-5.5` на уровнях `low`, `medium`, `high`, `xhigh` и `max`, с одним +исключением: `claude-opus-4.6` и `claude-sonnet-4.6` отклоняют нативный `xhigh`, поэтому этот +уровень сохраняет эмулируемый блок thinking. + +У Kiro нет уровня `minimal`. На девяти моделях выше, кроме `claude-opus-5` и `claude-opus-5.5`, +`minimal` сохраняет эмулируемый блок вместо отказа запросу, который работал до включения нативного +effort. Эти две модели Opus 5, а также модели GPT с нативным effort, отклоняют `minimal` ошибкой +400 `invalid_request_error` до отправки чего-либо. + +Поверхности сообщают об отказе по-разному. В запросе Responses в `error.message` попадает фраза с +названием модели и отклонённого уровня. На путях Messages и Chat Completions на этом месте стоит +фиксированный текст прокси `The provider rejected the request.`: оба пути заменяют подробность +адаптера собственным сообщением, а не передают её. Модель и уровень называет только Responses. +Потоковые и непоточные запросы читаются одинаково на всех поверхностях: отказ возникает при сборке +тела запроса к провайдеру, поэтому поток не открывается и вызов провайдера не происходит. + +Значение `ultra` от клиента при разборе заменяется на `max`. Настройки `pinnedReasoningEffort`, +`modelPinnedReasoningEfforts` и `modelPinnedEfforts` применяются позже: для моделей с нативным +Kiro effort адаптер также заменяет закреплённый `ultra` на `max`. Для моделей с эмуляцией такой +pin отбрасывается, и запрос отправляется без сигнала рассуждений. + +`claude-haiku-4.5`, `claude-opus-4.5`, `claude-sonnet-4.5` и любая не перечисленная выше +модель Kiro используют эмуляцию на каждом уровне; наличие настройки effort не доказывает нативную +поддержку. ## `cursor` diff --git a/docs-site/src/content/docs/tr/reference/adapters.md b/docs-site/src/content/docs/tr/reference/adapters.md index 5b75e305d39..11b870bf3f2 100644 --- a/docs-site/src/content/docs/tr/reference/adapters.md +++ b/docs-site/src/content/docs/tr/reference/adapters.md @@ -216,6 +216,26 @@ model listesi kanıtı bulunmayabilir. - `application/vnd.amazon.eventstream` kodunu çözer, metin/düşünme/araç olaylarını yeniden oluşturur, kesilmiş araç JSON'ını algılar ve yukarı akış belirteç sayılarını döndürmediği için kullanımı tahmin eder. +- İstek başına en fazla 48 istemci aracı ve 96 KB dönüştürülmüş araç JSON'ı gönderir, her araç + açıklamasını 1.024 karaktere (`gpt-5.6-sol` için 9.216) kısaltır. Tool search ile yüklenen + araçlar ve Codex code-mode yürütme aracı önce alınır, atlanan araçlar modele bildirilir. Proxy + ortamında `OCX_KIRO_TOOL_BUDGET=off` tüm istemci kataloğunu, `OCX_KIRO_TOOL_DESCRIPTION_LIMIT=off` + tam açıklamaları göndermenizi sağlar. İkisi de istek dönüştürülürken okunur, tüm Kiro modellerine + uygulanır ve diğer sınırları değiştirmez. Bütçeyi kapatmak sınırın kendisini ortadan kaldırır: + önceliklendirilecek ya da atlanacak bir şey kalmaz. Bildirilen tüm araçlar bildirim sırasıyla + gönderilir, atlama bildirimi kaybolur ve Kiro'nun kabul ettiğinden fazlasını bildiren istemci + kırpılmış katalog yerine yukarı akıştan `ValidationException` alır. İstek boyutu da istemci + kataloğuyla birlikte büyür. Bu anahtarlar daha büyük isteği kabul ettiği ölçülmüş bir hesap için + geri çekilme seçeneğidir; aşırı büyük bir kataloğu güvenli hale getirme yolu değildir. +- Kiro doğrulayıcısının reddettiği JSON Schema anahtar sözcüklerini (`pattern`, `minLength`, + `maximum`, `additionalProperties` ve 25 diğeri) araç ve yapılandırılmış çıktı şemalarından + çıkarır, ancak bir testin o modelde ölçtüğü anahtar sözcükleri bırakır. `claude-opus-4.5` + hepsini korur. `claude-opus-5.5` ve `claude-sonnet-5.5` yalnızca `pattern` ve `maximum` anahtar sözcüklerini korur; + diğerleri çıkarılmış kalır, çünkü hiçbir test bunları bu modellere göndermedi. Diğer Claude + kimlikleri dahil öteki Kiro modelleri tam çıkarmayı korur. İstisna model ve anahtar sözcük + bazındadır, çünkü bu hat akıl yürütme çabasında Claude modellerini zaten ayırıyor ve Kiro'nun + reddettiği bir anahtar sözcük yalnızca bir ipucunu kaybettirmekle kalmayıp tüm isteği + başarısız kılıyor. - Özel olduğunda yapılandırılmış `baseUrl`'i birebir kullanır. Kurallı bir `runtime.{region}.kiro.dev` URL'si içe aktarılan kimlik bilgisinin API bölgesini takip eder; bir uç nokta, imza, DNS veya bağlantı hatasından ya da çıktı başlamadan alınan HTTP 502/503/504 yanıtından sonra @@ -279,12 +299,40 @@ tam olarak tekrarlasa bile, çünkü aşama doğruluğu kozmetik tekilleştirmed ### Akıl yürütme çabası -GPT-5.6 ailesi `additionalModelRequestFields.reasoning.effort`, `claude-opus-5` ise -`additionalModelRequestFields.output_config.effort` alanını kullanır. `gpt-5.6-luna` ve -`gpt-5.6-terra` için yalnızca doğrulanmış `low`, `medium`, `high` ve `max` seviyeleri yerel alandan -gönderilir. Bu iki modelin yerel `xhigh` seviyesi doğrulanmadığı için mevcut sınırlı düşünme -talimatlarıyla öykünme korunur. `gpt-5.6-sol` ve `claude-opus-5` için mevcut yerel `low`, `medium`, -`high`, `xhigh` ve `max` davranışı değişmez. Diğer Kiro modelleri öykünme kullanır; çaba seçeneği yerel desteğin kanıtı değildir. +GPT-5.6 ailesi ve GPT-6 modelleri (`gpt-6-sol`, `gpt-6-luna`, `gpt-6.1-sol`, `gpt-6.1-terra`) +`additionalModelRequestFields.reasoning.effort`, aşağıda listelenen Claude modelleri ise +`additionalModelRequestFields.output_config.effort` alanını kullanır. +`gpt-5.6-luna` ve `gpt-5.6-terra` için yalnızca doğrulanmış `low`, `medium`, `high` ve `max` +seviyeleri yerel alandan gönderilir. Bu iki modelin yerel `xhigh` seviyesi doğrulanmadığı için mevcut +sınırlı düşünme talimatlarıyla öykünme korunur. `gpt-5.6-sol` ve GPT-6 modelleri yerel `low`, +`medium`, `high`, `xhigh` ve `max` davranışını aynı `reasoning.effort` alanı üzerinden sürdürür. + +Kiro `output_config.effort` alanını `claude-opus-5`, `claude-opus-5.5`, `claude-opus-4.8`, +`claude-opus-4.7`, `claude-opus-4.6`, `claude-sonnet-5.5`, `claude-sonnet-5`, `claude-sonnet-4.6` +ve `claude-haiku-5.5` için `low`, `medium`, `high`, `xhigh` ve `max` seviyelerinde kabul +eder. Tek istisna `claude-opus-4.6` ile `claude-sonnet-4.6` modellerinin yerel `xhigh` seviyesini +reddetmesidir; o seviye öykünülen düşünme bloğunu korur. + +Kiro'da `minimal` seviyesi yoktur. Yukarıdaki dokuz modelden `claude-opus-5` ve `claude-opus-5.5` +dışındakiler, yerel çaba açılmadan önce çalışan bir isteği başarısız kılmamak için `minimal` +seviyesinde öykünülen bloğu korur. Bu iki Opus 5 modeli ve yerel çaba kullanan GPT modelleri +`minimal` seviyesini hiçbir şey gönderilmeden önce 400 `invalid_request_error` ile reddeder. + +Yüzeyler bu reddi farklı bildirir. Responses isteği `error.message` alanına modeli ve reddedilen +seviyeyi belirten bir cümle koyar. Messages ve Chat Completions yolları aynı yere proxy'nin sabit +metnini, `The provider rejected the request.`, koyar; ikisi de adaptörün ayrıntısını olduğu gibi +aktarmak yerine kendi metniyle değiştirir. Modeli ve seviyeyi yalnızca Responses belirtir. Akışlı ve +akışsız istekler bütün yüzeylerde aynı okunur: ret, sağlayıcıya giden istek gövdesi kurulurken oluşur; +bu yüzden akış açılmaz ve sağlayıcıya çağrı yapılmaz. + +Çağıranın `ultra` değeri ayrıştırma sırasında `max` olur. `pinnedReasoningEffort`, +`modelPinnedReasoningEfforts` ve `modelPinnedEfforts` pinleri daha sonra uygulanır. Yerel Kiro +çabası kullanan modellerde adaptör pinlenmiş `ultra` değerini de `max` yapar. Öykünme kullanan +modellerde pin düşürülür ve istek akıl yürütme sinyali olmadan gönderilir. + +`claude-haiku-4.5`, `claude-opus-4.5`, `claude-sonnet-4.5` ve +yukarıda listelenmeyen Kiro modelleri her seviyede öykünme kullanır; çaba seçeneği yerel desteğin +kanıtı değildir. ## `cursor` diff --git a/docs-site/src/content/docs/zh-cn/reference/adapters.md b/docs-site/src/content/docs/zh-cn/reference/adapters.md index b337ce9da29..1f169c51e33 100644 --- a/docs-site/src/content/docs/zh-cn/reference/adapters.md +++ b/docs-site/src/content/docs/zh-cn/reference/adapters.md @@ -148,6 +148,21 @@ token。 JSON。上游不返回 token 数量,因此 usage 采用估算值。 - 经 `fetchResponse` 负责有界重试和分类/脱敏后的错误;非流式 parser 会排空同一 event stream, 供 web-search loop 使用。 +- 每次请求最多发送 48 个客户端工具、96 KB 的转换后工具 JSON,并把每个工具说明截断到 1,024 个字符 + (`gpt-5.6-sol` 为 9,216 个)。通过 tool search 加载的工具和 Codex code-mode 执行工具 + 优先入选,被省略的工具会告知模型。在代理环境中设置 `OCX_KIRO_TOOL_BUDGET=off` 可发送完整的 + 客户端目录,设置 `OCX_KIRO_TOOL_DESCRIPTION_LIMIT=off` 可发送完整说明。两者都在请求转换时读取, + 对所有 Kiro 模型生效,其他限制保持不变。关闭预算后限制本身消失,不再有需要排序或省略的对象: + 声明的全部工具按声明顺序上线,省略提示也随之消失,声明数量超过 Kiro 上限的客户端会收到上游 + `ValidationException`,而不是被裁剪的目录。请求大小也会随客户端目录增长。这些开关面向已实测 + 能接受更大请求的账户,并不能让过大的目录变得安全。 +- 移除 Kiro 校验器拒绝的 JSON Schema 关键字(`pattern`、`minLength`、`maximum`、 + `additionalProperties` 等 29 个),但保留探针在对应模型上实测通过的关键字。 + `claude-opus-4.5` 全部保留;`claude-opus-5.5` 与 `claude-sonnet-5.5` 只保留 + `pattern`、`maximum`,其余仍然移除, + 因为没有探针向这两个模型发送过其他关键字。包括其余 Claude id 在内的其他 Kiro 模型全部移除。 + 例外按模型和关键字划分,是因为这条 wire 在 reasoning effort 上已经区分 Claude 模型,而 Kiro + 拒绝的关键字不只是丢掉提示,还会让整个请求失败。 ### 完成与原生 stop reason Kiro 的 assistant 文本本身没有可靠的回合结束标记,但终止的 `metadataEvent` 可能带有原生 `stopReason`。 @@ -166,12 +181,36 @@ Kiro 的 assistant 文本本身没有可靠的回合结束标记,但终止的 ### Reasoning effort -GPT-5.6 系列使用 `additionalModelRequestFields.reasoning.effort`,`claude-opus-5` 使用 +GPT-5.6 系列和 GPT-6 系列(`gpt-6-sol`、`gpt-6-luna`、`gpt-6.1-sol`、`gpt-6.1-terra`)使用 +`additionalModelRequestFields.reasoning.effort`,下列 Claude 模型使用 `additionalModelRequestFields.output_config.effort`。`gpt-5.6-luna` 和 `gpt-5.6-terra` 仅通过原生字段发送已验证的 `low`、`medium`、`high` 和 `max`。 这两个模型的原生 `xhigh` 尚未验证,因此仍使用原有的有界 thinking 指令模拟。 -`gpt-5.6-sol` 和 `claude-opus-5` 保留现有原生档位(`low`、`medium`、`high`、`xhigh`、`max`)。 -其他 Kiro 模型使用模拟推理;提供 effort 选项并不代表原生支持。 +`gpt-5.6-sol` 和 GPT-6 系列通过同一个 `reasoning.effort` 字段保留原生 +`low`、`medium`、`high`、`xhigh`、`max`。 + +Kiro 对 `claude-opus-5`、`claude-opus-5.5`、`claude-opus-4.8`、`claude-opus-4.7`、 +`claude-opus-4.6`、`claude-sonnet-5.5`、`claude-sonnet-5`、`claude-sonnet-4.6` 和 +`claude-haiku-5.5` 在 `low`、`medium`、`high`、`xhigh` 和 `max` 上接受 +`output_config.effort`。有一个例外:`claude-opus-4.6` 和 `claude-sonnet-4.6` 拒绝原生 +`xhigh`,因此该档位保留模拟 thinking 块。 + +Kiro 没有 `minimal` 档位。除 `claude-opus-5` 和 `claude-opus-5.5` 外的上述九个模型在 +`minimal` 时保留模拟块,而不是让启用原生 effort 之前可用的请求失败。这两个 Opus 5 模型以及使用 +原生 effort 的 GPT 模型会在发送任何内容之前以 400 `invalid_request_error` 拒绝 `minimal`。 + +各接口表达这次拒绝的方式不同。Responses 请求把标明模型和被拒绝 effort 的句子放进 +`error.message`。Messages 与 Chat Completions 路径在该位置放代理自己的固定文案 +`The provider rejected the request.`,因为这两条路径不会转述适配器的细节,而是用自有文案替换。 +只有 Responses 会标明模型和 effort。流式与非流式在所有接口上返回相同内容:拒绝发生在构建上游 +请求体时,因此没有打开流,也没有发出上游调用。 + +调用方的 `ultra` 在解析时转换为 `max`。`pinnedReasoningEffort`、`modelPinnedReasoningEfforts` +和 `modelPinnedEfforts` 固定值在解析后应用;对于使用原生 Kiro effort 的模型,适配器也会将固定的 +`ultra` 转为 `max`。对于模拟推理模型,该固定值会被丢弃,请求不带推理信号发送。 + +`claude-haiku-4.5`、`claude-opus-4.5`、`claude-sonnet-4.5` 以及上面未列出的 Kiro +模型在每个 effort 上都使用模拟推理;提供 effort 选项并不代表原生支持。 ## `cursor` diff --git a/docs-site/src/content/docs/zh-tw/reference/adapters.md b/docs-site/src/content/docs/zh-tw/reference/adapters.md index 37ac0080cd5..eb1a59f8ac5 100644 --- a/docs-site/src/content/docs/zh-tw/reference/adapters.md +++ b/docs-site/src/content/docs/zh-tw/reference/adapters.md @@ -126,6 +126,21 @@ token。 JSON。上游不回傳 token 數量,因此 usage 採用估算值。 - 經 `fetchResponse` 負責有界重試和分類/脫敏後的錯誤;非流式 parser 會排空同一 event stream, 供 web-search loop 使用。 +- 每次請求最多傳送 48 個用戶端工具、96 KB 的轉換後工具 JSON,並把每個工具說明截斷到 1,024 個字元 + (`gpt-5.6-sol` 為 9,216 個)。透過 tool search 載入的工具和 Codex code-mode 執行工具 + 優先入選,被省略的工具會告知模型。在代理環境設定 `OCX_KIRO_TOOL_BUDGET=off` 可傳送完整的 + 用戶端目錄,設定 `OCX_KIRO_TOOL_DESCRIPTION_LIMIT=off` 可傳送完整說明。兩者都在請求轉換時讀取, + 對所有 Kiro 模型生效,其他限制維持不變。關閉預算後限制本身消失,不再有需要排序或省略的對象: + 宣告的全部工具依宣告順序上線,省略提示也隨之消失,宣告數量超過 Kiro 上限的用戶端會收到上游 + `ValidationException`,而不是被裁剪的目錄。請求大小也會隨用戶端目錄成長。這些開關面向已實測 + 能接受更大請求的帳戶,並不能讓過大的目錄變得安全。 +- 移除 Kiro 驗證器拒絕的 JSON Schema 關鍵字(`pattern`、`minLength`、`maximum`、 + `additionalProperties` 等 29 個),但保留探針在對應模型上實測通過的關鍵字。 + `claude-opus-4.5` 全部保留;`claude-opus-5.5` 與 `claude-sonnet-5.5` 只保留 + `pattern`、`maximum`,其餘仍然移除, + 因為沒有探針向這兩個模型傳送過其他關鍵字。包括其餘 Claude id 在內的其他 Kiro 模型全部移除。 + 例外按模型和關鍵字劃分,是因為這條 wire 在 reasoning effort 上已經區分 Claude 模型,而 Kiro + 拒絕的關鍵字不只是丟掉提示,還會讓整個請求失敗。 ### 完成與原生 stop reason @@ -146,12 +161,36 @@ Kiro 的 assistant 文字本身沒有可靠的回合結束標記,但終止的 ### Reasoning effort -GPT-5.6 系列使用 `additionalModelRequestFields.reasoning.effort`,`claude-opus-5` 使用 +GPT-5.6 系列和 GPT-6 系列(`gpt-6-sol`、`gpt-6-luna`、`gpt-6.1-sol`、`gpt-6.1-terra`)使用 +`additionalModelRequestFields.reasoning.effort`,下列 Claude 模型使用 `additionalModelRequestFields.output_config.effort`。`gpt-5.6-luna` 和 `gpt-5.6-terra` 只透過原生欄位傳送已驗證的 `low`、`medium`、`high` 和 `max`。 這兩個模型的原生 `xhigh` 尚未驗證,因此仍使用原有的有界 thinking 指令模擬。 -`gpt-5.6-sol` 和 `claude-opus-5` 保留現有原生檔位(`low`、`medium`、`high`、`xhigh`、`max`)。 -其他 Kiro 模型使用模擬推理;提供 effort 選項不代表原生支援。 +`gpt-5.6-sol` 和 GPT-6 系列透過同一個 `reasoning.effort` 欄位保留原生 +`low`、`medium`、`high`、`xhigh`、`max`。 + +Kiro 對 `claude-opus-5`、`claude-opus-5.5`、`claude-opus-4.8`、`claude-opus-4.7`、 +`claude-opus-4.6`、`claude-sonnet-5.5`、`claude-sonnet-5`、`claude-sonnet-4.6` 和 +`claude-haiku-5.5` 在 `low`、`medium`、`high`、`xhigh` 和 `max` 上接受 +`output_config.effort`。有一個例外:`claude-opus-4.6` 和 `claude-sonnet-4.6` 拒絕原生 +`xhigh`,因此該檔位保留模擬 thinking 區塊。 + +Kiro 沒有 `minimal` 檔位。除 `claude-opus-5` 和 `claude-opus-5.5` 外的上述九個模型在 +`minimal` 時保留模擬區塊,而不是讓啟用原生 effort 之前可用的請求失敗。這兩個 Opus 5 模型以及使用 +原生 effort 的 GPT 模型會在傳送任何內容之前以 400 `invalid_request_error` 拒絕 `minimal`。 + +各介面表達這次拒絕的方式不同。Responses 請求把標明模型與被拒絕 effort 的句子放進 +`error.message`。Messages 與 Chat Completions 路徑在該位置放代理自己的固定文案 +`The provider rejected the request.`,因為這兩條路徑不會轉述適配器的細節,而是用自有文案替換。 +只有 Responses 會標明模型與 effort。串流與非串流在所有介面上回傳相同內容:拒絕發生在建構上游 +請求體時,因此沒有開啟串流,也沒有發出上游呼叫。 + +呼叫方的 `ultra` 在解析時轉為 `max`。`pinnedReasoningEffort`、`modelPinnedReasoningEfforts` +和 `modelPinnedEfforts` 固定值在解析後套用;對使用原生 Kiro effort 的模型,適配器也會將固定的 +`ultra` 轉為 `max`。對使用模擬推理的模型,該固定值會被丟棄,請求不帶推理訊號送出。 + +`claude-haiku-4.5`、`claude-opus-4.5`、`claude-sonnet-4.5` 以及上面未列出的 Kiro +模型在每個 effort 上都使用模擬推理;提供 effort 選項不代表原生支援。 ## `cursor` diff --git a/scripts/test-layout/layout.json b/scripts/test-layout/layout.json index 7d2c5b21927..980ea7769da 100644 --- a/scripts/test-layout/layout.json +++ b/scripts/test-layout/layout.json @@ -1091,7 +1091,7 @@ "kiro-leased-responses.test.ts": "providers/kiro", "kiro-model-catalog.test.ts": "providers/kiro", "kiro-model-preference.test.ts": "providers/kiro", "kiro-oauth.test.ts": "providers/kiro", "oauth-kiro-refresh-attention.test.ts": "oauth", "kiro-org-user-identity.test.ts": "providers/kiro", - "kiro-pool-load-settings.test.ts": "providers/kiro", + "kiro-pool-load-settings.test.ts": "providers/kiro", "kiro-pinned-effort-ultra.test.ts": "providers/kiro", "kiro-pool-rank.test.ts": "providers/kiro", "kiro-reasoning-roundtrip.test.ts": "providers/kiro", "kiro-refusal-failover.test.ts": "providers/kiro", @@ -1102,7 +1102,7 @@ "kiro-metering-events.test.ts": "providers/kiro", "kiro-metering-usage.test.ts": "providers/kiro", "kiro-single-final.test.ts": "providers/kiro", "kiro-structured-output.test.ts": "providers/kiro", "kiro-structured-output-strict.test.ts": "providers/kiro", - "kiro-stream.test.ts": "providers/kiro", "kiro-tool-byte-accounting.test.ts": "providers/kiro", + "kiro-stream.test.ts": "providers/kiro", "kiro-tool-byte-accounting.test.ts": "providers/kiro", "kiro-tool-catalog.test.ts": "providers/kiro", "kiro-transport-parity.test.ts": "providers/kiro", "kiro-usage-quota.test.ts": "providers/kiro", "kiro-usage-restart.test.ts": "providers/kiro", diff --git a/src/adapters/kiro-tools.ts b/src/adapters/kiro-tools.ts index 4466ddf7133..9beaec58bd6 100644 --- a/src/adapters/kiro-tools.ts +++ b/src/adapters/kiro-tools.ts @@ -10,6 +10,10 @@ const MAX_KIRO_TOOL_DESCRIPTION_GPT_56_SOL = 9_216; // slots for Kiro's private completion tool and headroom for the enclosing request/message fields. export const MAX_KIRO_TOOL_COUNT = 48; export const MAX_KIRO_TOOL_CATALOG_BYTES = 96_000; +// Opt-out for operators whose Kiro account accepts the full client catalog; read per request. +export const KIRO_TOOL_BUDGET_ENV = "OCX_KIRO_TOOL_BUDGET"; +// Same opt-out for the per-tool description cap; read per request. +export const KIRO_TOOL_DESCRIPTION_LIMIT_ENV = "OCX_KIRO_TOOL_DESCRIPTION_LIMIT"; const textEncoder = new TextEncoder(); // JSON Schema validation/annotation keywords that Kiro's runtimeservice tool-spec validator @@ -17,6 +21,8 @@ const textEncoder = new TextEncoder(); // but the `memories__*` tools (add_ad_hoc_note/read/search/list) emit pattern/length/range // constraints via schemars, which trip the validator. Strip them everywhere in the schema tree; // the constraints are advisory for the model, so dropping them does not change tool behavior. +// PR #8451 reports keyword probes on claude-opus-4.5 and a constrained-tool probe on two +// 5.5 models. Other Claude ids have no reported schema-keyword measurements. const KIRO_REJECTED_SCHEMA_KEYS = new Set([ "additionalProperties", "pattern", @@ -65,28 +71,64 @@ const SCHEMA_MAP_KEYS = new Set(["properties", "$defs", "definitions"]); // silently changing the value a tool argument must equal. const SCHEMA_LITERAL_KEYS = new Set(["const", "enum", "default", "examples"]); -function sanitizeSchemaMap(value: unknown): unknown { - if (!value || typeof value !== "object" || Array.isArray(value)) return sanitizeKiroSchema(value); +const NO_SCHEMA_KEYS: ReadonlySet = new Set(); + +/** + * The rejected-key set minus `accepted`: the keywords one model id was measured to take. + * + * `KIRO_REJECTED_SCHEMA_KEYS` is the default; a probe can only widen what one id accepts, so an + * exception is written as the keys that probe covered, never as "everything". + */ +function rejectAllExcept(accepted: readonly string[]): ReadonlySet { + const keep = new Set(accepted); + return new Set([...KIRO_REJECTED_SCHEMA_KEYS].filter(key => !keep.has(key))); +} + +// PR #8451 reports the rejected keys above accepted on opus-4.5: its list enumerates 27 items +// because if/then/else stands for three keys, which is all 29 keys of the set. The report states +// that total as 28, one fewer than the 29 keys its own list names; the accepted keys, not the +// stated count, are what the exception below encodes. Its two 5.5 probes explicitly name only +// pattern and maximum. +// Other keywords and model ids retain the default strip until supported by evidence. +const KIRO_SCHEMA_KEY_EXCEPTIONS: ReadonlyMap> = new Map([ + ["claude-opus-4.5", NO_SCHEMA_KEYS], + ["claude-opus-5.5", rejectAllExcept(["pattern", "maximum"])], + ["claude-sonnet-5.5", rejectAllExcept(["pattern", "maximum"])], +]); + +/** + * Schema keywords Kiro rejects for `modelId`. + * + * Keys are dropped unless a probe measured that exact id taking them, and only the keys that probe + * covered survive. An id nobody probed behaves like every other Kiro model, which is the shape that + * shipped before this exception existed. + */ +export function kiroRejectedSchemaKeys(modelId: string): ReadonlySet { + return KIRO_SCHEMA_KEY_EXCEPTIONS.get(normalizeKiroModelId(modelId)) ?? KIRO_REJECTED_SCHEMA_KEYS; +} + +function sanitizeSchemaMap(value: unknown, rejected: ReadonlySet = KIRO_REJECTED_SCHEMA_KEYS): unknown { + if (!value || typeof value !== "object" || Array.isArray(value)) return sanitizeKiroSchema(value, rejected); // fromEntries defines own properties: a property named "__proto__" stays a property instead of // hitting the prototype setter and vanishing from the advertised schema. - return Object.fromEntries(Object.entries(value as Record).map(([name, child]) => [name, sanitizeKiroSchema(child)])); + return Object.fromEntries(Object.entries(value as Record).map(([name, child]) => [name, sanitizeKiroSchema(child, rejected)])); } /** Strip the keywords Kiro's tool-spec validator rejects, everywhere in a schema tree. */ -export function sanitizeKiroSchema(value: unknown): unknown { - if (Array.isArray(value)) return value.map(sanitizeKiroSchema); +export function sanitizeKiroSchema(value: unknown, rejected: ReadonlySet = KIRO_REJECTED_SCHEMA_KEYS): unknown { + if (Array.isArray(value)) return value.map(child => sanitizeKiroSchema(child, rejected)); if (!value || typeof value !== "object") return value; const out: Array<[string, unknown]> = []; for (const [key, child] of Object.entries(value as Record)) { - if (KIRO_REJECTED_SCHEMA_KEYS.has(key)) continue; + if (rejected.has(key)) continue; if (key === "required" && Array.isArray(child) && child.length === 0) continue; if (SCHEMA_LITERAL_KEYS.has(key)) out.push([key, child]); - else out.push([key, SCHEMA_MAP_KEYS.has(key) ? sanitizeSchemaMap(child) : sanitizeKiroSchema(child)]); + else out.push([key, SCHEMA_MAP_KEYS.has(key) ? sanitizeSchemaMap(child, rejected) : sanitizeKiroSchema(child, rejected)]); } return Object.fromEntries(out); } -function ensureRootObjectType(schema: unknown): Record { +function ensureRootObjectType(schema: unknown, rejected: ReadonlySet = KIRO_REJECTED_SCHEMA_KEYS): Record { const obj = schema && typeof schema === "object" && !Array.isArray(schema) ? schema as Record : {}; @@ -109,7 +151,7 @@ function ensureRootObjectType(schema: unknown): Record { // Seed with the root's own properties/required so a schema like // { type:"object", properties:{path}, required:["path"], oneOf:[...] } keeps them. if (obj.properties && typeof obj.properties === "object") { - Object.assign(props, sanitizeSchemaMap(obj.properties) as Record); + Object.assign(props, sanitizeSchemaMap(obj.properties, rejected) as Record); } if (Array.isArray(obj.required)) { for (const r of obj.required) if (typeof r === "string") required.add(r); @@ -124,7 +166,7 @@ function ensureRootObjectType(schema: unknown): Record { if (!variant || typeof variant !== "object" || Array.isArray(variant)) continue; const v = variant as Record; if (v.properties && typeof v.properties === "object") { - Object.assign(props, sanitizeSchemaMap(v.properties) as Record); + Object.assign(props, sanitizeSchemaMap(v.properties, rejected) as Record); } if (mergeRequired && Array.isArray(v.required)) { for (const r of v.required) if (typeof r === "string") required.add(r); @@ -147,6 +189,7 @@ function ensureRootObjectType(schema: unknown): Record { } function toolDescriptionLimit(modelId: string): number { + if (process.env[KIRO_TOOL_DESCRIPTION_LIMIT_ENV] === "off") return Number.POSITIVE_INFINITY; return normalizeKiroModelId(modelId) === "gpt-5.6-sol" ? MAX_KIRO_TOOL_DESCRIPTION_GPT_56_SOL : MAX_KIRO_TOOL_DESCRIPTION_UNVERIFIED; @@ -196,6 +239,7 @@ export function convertKiroToolContext( ): { tools: unknown[]; systemAdditions: string[]; nameMap: Map; registry: KiroToolNameRegistry } { const tools = parsed.context.tools ?? []; const descriptionLimit = toolDescriptionLimit(parsed.modelId); + const rejectedSchemaKeys = kiroRejectedSchemaKeys(parsed.modelId); // Validate every listed name even when tool_choice:none emulates a tool-free turn. for (const tool of tools) registry.alias(namespacedToolName(tool.namespace, tool.name)); const effectiveTools = parsed.options.toolChoice === "none" ? [] : tools; @@ -211,13 +255,14 @@ export function convertKiroToolContext( toolSpecification: { name: toolName, description: truncateDescription(description, descriptionLimit), - inputSchema: { json: ensureRootObjectType(sanitizeKiroSchema(tool.parameters ?? {})) }, + inputSchema: { json: ensureRootObjectType(sanitizeKiroSchema(tool.parameters ?? {}, rejectedSchemaKeys), rejectedSchemaKeys) }, }, }; return { tool, index, converted }; }); - const exceedsBudget = convertedEntries.length > MAX_KIRO_TOOL_COUNT - || serializedToolCatalogBytes(convertedEntries.map(entry => entry.converted)) > MAX_KIRO_TOOL_CATALOG_BYTES; + const unbounded = process.env[KIRO_TOOL_BUDGET_ENV] === "off"; + const exceedsBudget = !unbounded && (convertedEntries.length > MAX_KIRO_TOOL_COUNT + || serializedToolCatalogBytes(convertedEntries.map(entry => entry.converted)) > MAX_KIRO_TOOL_CATALOG_BYTES); const candidates = exceedsBudget ? convertedEntries.toSorted((a, b) => boundedCatalogPriority(a.tool) - boundedCatalogPriority(b.tool) || a.index - b.index) : convertedEntries; @@ -241,8 +286,9 @@ export function convertKiroToolContext( // subtracting a standalone size would misjudge it by the separators JSON adds between entries. const projected = reserved ? [...filled, entry.converted, reserved.converted] : [...filled, entry.converted]; if ( - projected.length > MAX_KIRO_TOOL_COUNT - || serializedToolCatalogBytes(projected) > MAX_KIRO_TOOL_CATALOG_BYTES + !unbounded + && (projected.length > MAX_KIRO_TOOL_COUNT + || serializedToolCatalogBytes(projected) > MAX_KIRO_TOOL_CATALOG_BYTES) ) break; filled.push(entry.converted); admitted.add(entry.index); diff --git a/src/adapters/kiro/payload.ts b/src/adapters/kiro/payload.ts index f3221ed37c4..937127f359e 100644 --- a/src/adapters/kiro/payload.ts +++ b/src/adapters/kiro/payload.ts @@ -39,6 +39,7 @@ import { } from "./conversation"; import { injectKiroThinkingTags, + kiroEmulatesUnsupportedEffort, kiroNativeEffortField, kiroReasoningContent, KIRO_NATIVE_EFFORTS, @@ -199,8 +200,9 @@ export function buildKiroPayload( : undefined; // A tool-free structured-output turn advertises only its output channel. The catalog nudge is a // work-tool contract (shell, file search, result counting) and would describe tools that do not - // exist, so it is reserved for turns that carry real tools. - const toolCatalogNudge = structuredOutput && ordinaryTools.length === 0 + // exist, so it is reserved for turns that carry real tools. Claude Code and Claude Desktop send + // their own tool rules with the full catalog, so the Codex-oriented nudge only repeats it. + const toolCatalogNudge = (structuredOutput && ordinaryTools.length === 0) || parsed._anthropicInbound ? undefined : buildNonOpenAIToolCatalogNudgeFromNames( kiroToolWireNames(kiroTools), @@ -483,10 +485,11 @@ export function buildKiroPayload( ...(history.length > 0 ? { history } : {}), }, }; - const effort = parsed.options.reasoning; const effortField = kiroNativeEffortField(parsed.modelId); + const effort = effortField && parsed.options.reasoning === "ultra" && KIRO_NATIVE_EFFORTS.includes("max") + ? "max" : parsed.options.reasoning; if (effortField && effort && effort !== "none") { - if (!KIRO_NATIVE_EFFORTS.includes(effort)) { + if (!KIRO_NATIVE_EFFORTS.includes(effort) && !kiroEmulatesUnsupportedEffort(parsed.modelId, effort)) { throw new Error(`Kiro ${normalizeKiroModelId(parsed.modelId)} does not support reasoning effort ${JSON.stringify(effort)}`); } // Model eligibility still owns unsupported-effort validation above; wire eligibility diff --git a/src/adapters/kiro/reasoning.ts b/src/adapters/kiro/reasoning.ts index c8f865b8d54..f38c8e238ce 100644 --- a/src/adapters/kiro/reasoning.ts +++ b/src/adapters/kiro/reasoning.ts @@ -30,9 +30,19 @@ export const KIRO_NATIVE_EFFORT_FIELDS: Record; /** True when the caller asked for `strict: true`, so the value must conform to the whole schema. */ strict?: boolean; - /** The tool input schema actually advertised: sanitized, and wrapped when `wrapped` is true. */ + /** The tool input schema actually advertised: stripped of what this model rejects, and wrapped when `wrapped` is true. */ toolSchema: Record; /** * True when the answer rides the tool input's `value` property instead of BEING the input. @@ -79,7 +79,7 @@ export function kiroStructuredOutputSpec(parsed: OcxParsedRequest): KiroStructur if (!schema) return { toolSchema: { type: "object" }, wrapped: false, ...meta }; if (strict) assertKiroStrictSchemaSupported(schema); const contract = { schema, ...(strict ? { strict } : {}) }; - const sanitized = sanitizeKiroSchema(schema) as JsonRecord; + const sanitized = sanitizeKiroSchema(schema, kiroRejectedSchemaKeys(parsed.modelId)) as JsonRecord; if (isPlainObjectRoot(schema)) return { ...contract, toolSchema: sanitized, wrapped: false, ...meta }; // `#/$defs/...` references resolve against the document root, which is now the wrapper. Hoist the // definitions with them so a referenced schema still resolves after it moves under `value`, and @@ -102,7 +102,7 @@ export function kiroStructuredOutputSpec(parsed: OcxParsedRequest): KiroStructur }; } -/** The private terminal tool advertised for a structured-output turn, with the caller's (sanitized, possibly wrapped) schema as its input schema. */ +/** The private terminal tool advertised for a structured-output turn, with the caller's (possibly stripped, possibly wrapped) schema as its input schema. */ export function kiroStructuredOutputTool(spec: KiroStructuredOutputSpec): Record { const delivery = spec.wrapped ? "Put the complete final JSON value in `value`." @@ -122,10 +122,12 @@ export function kiroStructuredOutputTool(spec: KiroStructuredOutputSpec): Record /** * The prose half of the structured-output contract, at most `maxChars` long. * - * The caller's schema is restated verbatim because the advertised one is sanitized: Kiro's - * validator rejects `additionalProperties`, `pattern`, length and range keywords, so the tool schema - * alone would let the model miss constraints the caller wrote. A schema that does not fit the - * injected-instruction budget is dropped whole rather than cut mid-JSON. + * The caller's schema is restated verbatim because the advertised one is sanitized for most models: + * `kiroRejectedSchemaKeys` drops `additionalProperties`, `pattern`, length and range keywords + * unless a probe measured that model id taking them, so the tool schema alone can leave the model + * without constraints the caller wrote. Keywords the model does keep are repeated here as well. A + * schema that does not fit the injected-instruction budget is dropped whole rather than cut + * mid-JSON. */ export function kiroStructuredOutputInstructions( spec: KiroStructuredOutputSpec, diff --git a/src/providers/kiro-models.ts b/src/providers/kiro-models.ts index a940e09d792..38ea0fc7e54 100644 --- a/src/providers/kiro-models.ts +++ b/src/providers/kiro-models.ts @@ -12,12 +12,10 @@ export const KIRO_MODELS = [ "gpt-6.1-sol", // 261027 preemptive: GPT-6.1 Terra (mirrors Kiro's GPT-6.1 Sol seed; provider listing unverified). "gpt-6.1-terra", - // 260929 preemptive: Claude Sonnet 5.5 added ahead of Kiro's catalog (kiro.dev did not list it on - // 2026-09-29). Mirrors claude-sonnet-5; calls fail upstream until Kiro ships the model. + // Listed on kiro.dev/docs/models (2026-10-24). "claude-sonnet-5.5", "claude-sonnet-5", - // 260923 preemptive: Claude Opus 5.5 added ahead of Kiro's catalog (kiro.dev did not list it on - // 2026-09-23). Mirrors claude-opus-5; calls fail upstream until Kiro ships the model. + // Listed on kiro.dev/docs/models (2026-10-24). "claude-opus-5.5", "claude-opus-5", "claude-opus-4.8", @@ -27,7 +25,7 @@ export const KIRO_MODELS = [ "claude-sonnet-4.6", "claude-sonnet-4.5", "claude-sonnet-4.0", - // 261008 preemptive: Haiku 5.5; calls fail upstream until Kiro ships it. + // Listed on kiro.dev/docs/models (2026-10-24); two-tier pricing above 100K input tokens. "claude-haiku-5.5", "claude-haiku-4.5", "deepseek-3.2", diff --git a/src/server/responses/request-prepare.ts b/src/server/responses/request-prepare.ts index deef67f2558..f59a7bdb63c 100644 --- a/src/server/responses/request-prepare.ts +++ b/src/server/responses/request-prepare.ts @@ -541,6 +541,7 @@ export async function prepareResponsesRequest( parsed._anthropicServerTools = options.anthropicServerTools; parsed._anthropicServerReplay = options.anthropicServerReplay; parsed._anthropicSystemCachePrefix = options.anthropicSystemCachePrefix; + parsed._anthropicInbound = true; if (parsed.options.textFormat?.type === "json_schema") parsed._structuredOutputConformance = true; } parsed._promptCacheKeyIsSharedCohort = options.promptCacheKeyIsSharedCohort; diff --git a/src/types/request.ts b/src/types/request.ts index 227625bf9e2..38f30c6c3eb 100644 --- a/src/types/request.ts +++ b/src/types/request.ts @@ -93,6 +93,8 @@ export interface OcxParsedRequest { * adapter ends a system block there so the breakpoint stays off the per-turn tail. */ _anthropicSystemCachePrefix?: string; + /** The request arrived on the Anthropic Messages surface (Claude Code, Claude Desktop). */ + _anthropicInbound?: boolean; /** * Boundary between replayed history and this turn's newly appended input. Usually the * items the proxy restored from local previous_response_id state; also set when the diff --git a/structure/providers/kiro.md b/structure/providers/kiro.md index 547ee8458e4..079b4d94b2c 100644 --- a/structure/providers/kiro.md +++ b/structure/providers/kiro.md @@ -133,6 +133,69 @@ Kiro's own `kiroToolName` rewrite in `src/adapters/kiro-wire.ts` is CodeWhispere reserves the private completion tool. Meta Muse 64-character MCP aliases live in `src/responses/muse-tool-name-alias.ts` and must not import that Kiro helper. +## Kiro tool catalog and schema policy + +`convertKiroToolContext` in `src/adapters/kiro-tools.ts` builds the outbound client-tool +catalog, and two limits apply there per request: at most `MAX_KIRO_TOOL_COUNT` (48) converted +tools and `MAX_KIRO_TOOL_CATALOG_BYTES` (96 KB) of serialized tool JSON. Above either limit the +catalog is sorted by `boundedCatalogPriority` — results loaded through tool search, then the +Codex code-mode `exec` tool, then the tool-search gateway, then the rest in declaration order — +and admitted until the next tool would not fit. The omitted tools are named in one +`systemAdditions` notice so the model knows what it cannot call; the notice is not an error and +the request still goes out. + +Each tool description is cut to 1,024 characters, or 9,216 for `gpt-5.6-sol`, with a +surrogate-safe ellipsis. Truncation happens while each tool is converted, before the budget +chooses entries, so the byte limit measures the truncated strings. + +`OCX_KIRO_TOOL_BUDGET=off` (`KIRO_TOOL_BUDGET_ENV`) and +`OCX_KIRO_TOOL_DESCRIPTION_LIMIT=off` (`KIRO_TOOL_DESCRIPTION_LIMIT_ENV`) are operator opt-outs +read at request-conversion time. Turning the budget off removes both limits, so the priority sort +and the `exec` reservation have nothing to exclude: every declared tool goes on the wire in +declaration order and no omission notice is emitted. An oversized catalog therefore reaches Kiro +intact and the upstream `ValidationException` becomes the failure mode instead of a trimmed +request. The default remains bounded because that is the shape a fresh Kiro account is measured to +accept. + +Schema sanitization is chosen per model id and per keyword, not per family or transport. +`kiroRejectedSchemaKeys(modelId)` returns `KIRO_REJECTED_SCHEMA_KEYS` for every id except those in +`KIRO_SCHEMA_KEY_EXCEPTIONS`, which maps an id to the rejected keys minus the ones a probe measured +it taking. An exception can only widen what a probe covered; it is never "this model accepts +everything". + +PR #8451 reports an opus-4.5 probe covering every rejected keyword, so its exception is the empty +set. Its list writes 27 items because `if/then/else` stands for three keys, which is exactly the +29 keys of the set, while the report states that total as 28 — one fewer than the keys its list +names. The accepted keys, not the stated count, are what the exception encodes. It reports a +constrained tool probe on `claude-opus-5.5` and `claude-sonnet-5.5` naming `pattern` and +`maximum`; only those two keys survive for those ids. The report does not establish the other +schema keys. + +The remaining Claude ids on this provider (`claude-opus-5`, `claude-opus-4.8`, +`claude-opus-4.7`, `claude-opus-4.6`, `claude-sonnet-5`, `claude-sonnet-4.6`, +`claude-sonnet-4.5`, `claude-sonnet-4.0`, `claude-haiku-5.5`, `claude-haiku-4.5`) were not +probed for schema keywords and keep the full strip. The scope is deliberate: this wire already +separates Claude models on another field — Haiku 4.5, Opus 4.5 and Sonnet 4.5 reject +`output_config.effort` outright and the 4.6 pair rejects `xhigh` — so a family-wide promise would +be an inference, and a wrong one surfaces as an upstream `ValidationException` for the whole +request rather than a lost hint. The prose contract restates the original schema only when it fits +the instruction budget; otherwise stripped constraints are absent from the model-facing +structured-output request. + +Both the client-tool path and the structured-output tool (`kiroStructuredOutputSpec` in +`src/adapters/kiro/structured-output.ts`) pass that set into `sanitizeKiroSchema`, so the two +cannot drift. Root +`oneOf`/`anyOf`/`allOf` flattening in `ensureRootObjectType` stays unconditional for every +model: it is a wire requirement, not a constraint strip. + +The Codex tool-catalog nudge is scoped by inbound surface, not by routed model. Requests prepared +from the Anthropic Messages wire carry `_anthropicInbound` +(`src/server/responses/request-prepare.ts`), and `src/adapters/kiro/payload.ts` omits the nudge +for them: its text names Codex-only concepts (code mode, neighboring-agent tool bans) that a +Claude Code or Claude Desktop turn does not have, and every such client sends its own tool rules. +A non-Claude model routed from that surface therefore drops the nudge too, while the same model +keeps it on the Codex surface. Coverage: `tests/providers/kiro/kiro-tool-catalog.test.ts`. + ## Kiro Responses text controls Kiro shares the Responses freeform restoration boundary in @@ -152,7 +215,8 @@ comes from `output_config.format`) is a contract the CodeWhisperer wire has no r `src/adapters/kiro/structured-output.ts` emulates it with the private terminal tool `codex_kiro_structured_output`, reserved against client tools in `src/adapters/kiro-wire.ts`: -- The tool's input schema is the caller's schema, sanitized like every Kiro tool schema. A root that +- The tool's input schema is the caller's schema, stripped of the keywords `kiroRejectedSchemaKeys` + lists for that model id, exactly as a client tool schema is. A root that is not a plain `type: "object"` (array, scalar, root `anyOf`/`oneOf`/`allOf`/`$ref`) is wrapped as `{value: }` with `$defs`/`definitions` hoisted and every other local `$ref` (`#`, `#/items`, ...) re-pointed under `#/properties/value`, rather than flattened the way @@ -290,10 +354,42 @@ on the matching `assistantResponseMessage.reasoningContent` to preserve model re turns; dropping it makes every turn restart without the previous turn's reasoning. Verified on kiro-cli 2.14.1 and 2.16.0, all three models. -Native effort admission is narrower than model eligibility: luna and terra send only -`low`, `medium`, `high`, and `max` on the native field. Their `xhigh` requests retain the -previous emulated thinking tags because that native rung is unverified. A future shared -effort rung does not expand this allowlist. Sol and Opus keep their existing native ladder. +Native effort admission is narrower than model eligibility, and the narrowing lives in +`kiroNativeEffortField` rather than in `KIRO_NATIVE_EFFORT_FIELDS` alone +(`src/adapters/kiro/reasoning.ts`): a model can be in the field table and still send an +individual rung on the emulated path. Luna and terra send only `low`, `medium`, `high`, and +`max` on the native field, and their `xhigh` keeps the previous emulated thinking tags because +that native rung is unverified. On the Claude side, `claude-opus-4.6` and `claude-sonnet-4.6` +reject a native `xhigh` (HTTP 400 in the 2026-11-07 probe), so `KIRO_NO_XHIGH_NATIVE_MODELS` +routes that rung to the emulated block instead. + +`KIRO_NATIVE_EFFORTS` is `low`, `medium`, `high`, `xhigh`, `max`. For `minimal` +`buildKiroPayload` throws, and the request becomes HTTP 400 `invalid_request_error` before +anything is sent, unless `kiroEmulatesUnsupportedEffort` accepts the model. Responses includes +the adapter detail; Messages and Chat Completions use the fixed request-rejected message. +`minimal` is the only outside-ladder value a caller's own request can deliver: the +Responses parser folds `ultra` to `max` for both surfaces — the Messages translator copies +`output_config.effort` through verbatim and hands the translated body to the same parser — and +combo resolution draws from the catalog's `low`..`max` ladder. The emulation set is the seven +Claude ids #8451 moved to native effort — +`claude-opus-4.8`, `claude-opus-4.7`, `claude-opus-4.6`, `claude-sonnet-5.5`, `claude-sonnet-5`, +`claude-sonnet-4.6`, `claude-haiku-5.5` — and they emulated every effort before the change, so +they keep the emulated block for `minimal` instead of failing a request they used to serve. +`claude-opus-5` and `claude-opus-5.5` are deliberately absent: they had native effort before the +change, so `minimal` stays an error for them, as it is for the GPT models with native effort. + +An operator pin is applied after parsing by `applyPinnedEffort` (`src/server/effort-policy.ts`), +so `ultra` reaches `buildKiroPayload`. For models in `KIRO_NATIVE_EFFORT_FIELDS`, the builder +folds it to native `max` before validating and emitting the wire field. Models outside the table +keep the emulated path: `kiroThinkingBudget` has no `ultra` entry, so the request is sent without +a reasoning signal. Other unsupported values still fail native validation. +`tests/providers/kiro/kiro-pinned-effort-ultra.test.ts` covers the pin forms and client surfaces. + +The split is operator-visible. A `minimal` request reaches the seven as the emulated thinking +block with no error, while the same request to Opus 5, Opus 5.5 or a native GPT model is a 400 +`invalid_request_error` with the surface split described above. This is an enumerated fallback, +not a general unknown-effort path: a value outside both the ladder and this set still throws. A +future shared effort rung does not widen any of these allowlists. The two members of `reasoningContent` are not interchangeable. The wire validates the shape of the member rather than its content, and the signature is not base64 — its alphabet contains `.` and diff --git a/tests/fixtures/test-layout-expected.json b/tests/fixtures/test-layout-expected.json index 86245f9f4cf..d90ae345f86 100644 --- a/tests/fixtures/test-layout-expected.json +++ b/tests/fixtures/test-layout-expected.json @@ -983,7 +983,7 @@ "kiro-device-social.test.ts": "providers/kiro", "kiro-fallback-error-body.test.ts": "providers/kiro", "kiro-images.test.ts": "providers/kiro", "kiro-leased-responses.test.ts": "providers/kiro", "kiro-completion-retry-wording.test.ts": "providers/kiro", "kiro-model-catalog.test.ts": "providers/kiro", "kiro-model-preference.test.ts": "providers/kiro", - "kiro-oauth.test.ts": "providers/kiro", "oauth-kiro-refresh-attention.test.ts": "oauth", "kiro-pool-load-settings.test.ts": "providers/kiro", + "kiro-oauth.test.ts": "providers/kiro", "oauth-kiro-refresh-attention.test.ts": "oauth", "kiro-pool-load-settings.test.ts": "providers/kiro", "kiro-pinned-effort-ultra.test.ts": "providers/kiro", "kiro-org-user-identity.test.ts": "providers/kiro", "kiro-content-filter.test.ts": "providers/kiro", "kiro-pool-rank.test.ts": "providers/kiro", "kiro-reasoning-roundtrip.test.ts": "providers/kiro", "kiro-refusal-failover.test.ts": "providers/kiro", "kiro-refusal.test.ts": "providers/kiro", @@ -992,6 +992,7 @@ "kiro-metering-usage.test.ts": "providers/kiro", "kiro-completion-with-real-tools.test.ts": "providers/kiro", "kiro-single-final.test.ts": "providers/kiro", "kiro-structured-output.test.ts": "providers/kiro", "kiro-structured-output-strict.test.ts": "providers/kiro", "kiro-stream.test.ts": "providers/kiro", "kiro-tool-byte-accounting.test.ts": "providers/kiro", "kiro-transport-parity.test.ts": "providers/kiro", + "kiro-tool-catalog.test.ts": "providers/kiro", "kiro-usage-quota.test.ts": "providers/kiro", "kiro-usage-restart.test.ts": "providers/kiro", "kiro-windows-cli-db-path.test.ts": "providers/kiro", "kiro-windows-cli-executable-path.test.ts": "providers/kiro", "kiro-wire-estimate.test.ts": "providers/kiro", "lab-activation.test.ts": "lab", diff --git a/tests/providers/kiro/kiro-adapter.test.ts b/tests/providers/kiro/kiro-adapter.test.ts index aee5513f886..29f0badfb1a 100644 --- a/tests/providers/kiro/kiro-adapter.test.ts +++ b/tests/providers/kiro/kiro-adapter.test.ts @@ -784,7 +784,7 @@ describe("kiro adapter — buildRequest", () => { }, }; const { body } = await createKiroAdapter(provider).buildRequest( - parsedWith([{ role: "user", content: "hi" }], [{ name: "bash", description: "Run command", parameters }]), + parsedWith([{ role: "user", content: "hi" }], [{ name: "bash", description: "Run command", parameters }], "gpt-5.6-sol"), ); const schema = JSON.parse(body).conversationState.currentMessage.userInputMessage.userInputMessageContext.tools[0].toolSpecification.inputSchema.json; @@ -811,7 +811,7 @@ describe("kiro adapter — buildRequest", () => { required: ["filename", "note"], }; const { body } = await createKiroAdapter(provider).buildRequest( - parsedWith([{ role: "user", content: "hi" }], [{ name: "memories__add_ad_hoc_note", description: "Remember", parameters }]), + parsedWith([{ role: "user", content: "hi" }], [{ name: "memories__add_ad_hoc_note", description: "Remember", parameters }], "gpt-5.6-sol"), ); const schema = JSON.parse(body).conversationState.currentMessage.userInputMessage.userInputMessageContext.tools[0].toolSpecification.inputSchema.json; @@ -845,7 +845,7 @@ describe("kiro adapter — buildRequest", () => { required: ["target", "message"], }; const { body } = await createKiroAdapter(provider).buildRequest( - parsedWith([{ role: "user", content: "hi" }], [{ name: "followup_task", namespace: "collaboration", description: "Send follow-up", parameters }]), + parsedWith([{ role: "user", content: "hi" }], [{ name: "followup_task", namespace: "collaboration", description: "Send follow-up", parameters }], "gpt-5.6-sol"), ); const schema = JSON.parse(body).conversationState.currentMessage.userInputMessage.userInputMessageContext.tools[0].toolSpecification.inputSchema.json; @@ -867,7 +867,7 @@ describe("kiro adapter — buildRequest", () => { $defs: { Inner: { type: "object", properties: { id: { type: "string" } } } }, }; const { body } = await createKiroAdapter(provider).buildRequest( - parsedWith([{ role: "user", content: "hi" }], [{ name: "memories__read", description: "Read", parameters }]), + parsedWith([{ role: "user", content: "hi" }], [{ name: "memories__read", description: "Read", parameters }], "gpt-5.6-sol"), ); const schema = JSON.parse(body).conversationState.currentMessage.userInputMessage.userInputMessageContext.tools[0].toolSpecification.inputSchema.json; @@ -954,7 +954,7 @@ describe("kiro adapter — buildRequest", () => { test("root composition preserves root properties/siblings and merges coexisting keywords", async () => { const pick = async (schema: unknown) => JSON.parse((await createKiroAdapter(provider).buildRequest( - parsedWith([{ role: "user", content: "hi" }], [{ name: "comp2", description: "d", parameters: schema }]), + parsedWith([{ role: "user", content: "hi" }], [{ name: "comp2", description: "d", parameters: schema }], "gpt-5.6-sol"), )).body).conversationState.currentMessage.userInputMessage.userInputMessageContext.tools[0].toolSpecification.inputSchema.json; // Root direct properties/required AND a sibling oneOf: keep the root fields, merge the variant. @@ -1660,7 +1660,7 @@ describe("kiro adapter — native and emulated reasoning effort", () => { test("mid-turn steering reaches Kiro identically for opus-5 and opus-4.8", async () => { // The #543 reporter observed opus-4.8 honoring mid-turn steers while opus-5 ignored them on // the same proxy build. Pin that our request construction does not differ between the two - // beyond model identity and opus-5's native effort field, so a future model-conditional + // beyond model identity and the native effort field both now carry, so a future model-conditional // regression on this path is caught here rather than in a user's session. const steer = "Stop and switch approach now."; const messages = [ @@ -1686,10 +1686,9 @@ describe("kiro adapter — native and emulated reasoning effort", () => { { content: [{ text: "out" }], status: "success", toolUseId: "call-1" }, ]); } - // Only the native-effort field may differ; opus-4.8 also gets no emulated thinking tags - // here because tool-result turns skip that injection. + // Both carry the same native effort field, and neither gets emulated thinking tags. expect(opus5.additionalModelRequestFields).toEqual({ output_config: { effort: "high" } }); - expect(opus48.additionalModelRequestFields).toBeUndefined(); + expect(opus48.additionalModelRequestFields).toEqual({ output_config: { effort: "high" } }); expect(opus48.conversationState.currentMessage.userInputMessage.content).not.toContain(""); }); @@ -1728,6 +1727,23 @@ describe("kiro adapter — native and emulated reasoning effort", () => { })).rejects.toThrow(`Kiro ${modelId} does not support reasoning effort "minimal"`); } }); + + test("Claude models newly given native effort keep the emulated block for minimal", async () => { + for (const modelId of ["claude-opus-4.8", "claude-opus-4.7", "claude-opus-4.6", "claude-sonnet-5.5", "claude-sonnet-5", "claude-sonnet-4.6", "claude-haiku-5.5"]) { + const body = JSON.parse((await createKiroAdapter(provider).buildRequest({ + ...parsedWith([{ role: "user", content: "solve" }], undefined, modelId), + options: { reasoning: "minimal", maxOutputTokens: 1000 }, + })).body); + + expect(body.additionalModelRequestFields).toBeUndefined(); + // Both halves of the emulated block, not just the budget: a fallback that only prepended the + // length tag would leave `minimal` below the model's own thinking default without saying so. + const content = body.conversationState.currentMessage.userInputMessage.content; + expect(content).toContain("enabled"); + expect(content).toContain("100"); + expect(content).toContain(""); + } + }); }); describe("kiro adapter — per-model context windows (kiro.dev/docs/models)", () => { diff --git a/tests/providers/kiro/kiro-pinned-effort-ultra.test.ts b/tests/providers/kiro/kiro-pinned-effort-ultra.test.ts new file mode 100644 index 00000000000..2917a1359cb --- /dev/null +++ b/tests/providers/kiro/kiro-pinned-effort-ultra.test.ts @@ -0,0 +1,248 @@ +import { afterEach, beforeEach, expect, test } from "bun:test"; +import { mkdtempSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { getAccountSet, saveCredential, setActiveAccount } from "../../../src/oauth/store"; +import { handleChatCompletions } from "../../../src/server/chat-completions"; +import { handleClaudeMessages } from "../../../src/server/claude-messages"; +import { handleResponses } from "../../../src/server/responses"; +import type { OcxConfig } from "../../../src/types"; +import { acquireOwnedSpendHome } from "../../helpers/owned-spend-home"; +import { installIsolatedCodexHome, type IsolatedCodexHome } from "../../helpers/isolated-codex-home"; +import { removeTreeWithRetry } from "../../helpers/remove-tree"; + +const realFetch = globalThis.fetch; +const oldOcxHome = process.env.OPENCODEX_HOME; +let home = ""; +let isolated: IsolatedCodexHome; +let releaseSpend: () => void; + +beforeEach(async () => { + home = mkdtempSync(join(tmpdir(), "ocx-kiro-ultra-pin-")); + isolated = installIsolatedCodexHome("ocx-kiro-ultra-codex-"); + process.env.OPENCODEX_HOME = home; + releaseSpend = acquireOwnedSpendHome(); + await saveCredential("kiro", { + access: "synthetic-access", refresh: "synthetic-refresh", expires: Date.now() + 3_600_000, + accountId: "ultra-pin-account", source: "oauth", + kiro: { profileArn: "arn:aws:codewhisperer:us-east-1:123456789012:profile/0", apiRegion: "us-east-1" }, + }, { addAccount: true }); + await setActiveAccount("kiro", getAccountSet("kiro")!.accounts[0]!.id); +}); +afterEach(() => { + // Ledger lease first: an open SQLite handle inside a directory being removed is a teardown race. + releaseSpend(); + globalThis.fetch = realFetch; + if (oldOcxHome === undefined) delete process.env.OPENCODEX_HOME; + else process.env.OPENCODEX_HOME = oldOcxHome; + isolated.restore(); + removeTreeWithRetry(home); +}); + +/** A provider pin is an operator setting; `ultra` is a declared effort, so config admits it. */ +type Pin = string | { form: "provider" | "model" | "global"; effort: string }; +function config(models: string[], pin?: Pin): OcxConfig { + const form = typeof pin === "string" ? "provider" : pin?.form; + const effort = typeof pin === "string" ? pin : pin?.effort; + return { port: 0, defaultProvider: "kiro", + ...(form === "global" ? { modelPinnedEfforts: { [models[0]!]: effort } } : {}), + providers: { kiro: { + adapter: "kiro", authMode: "oauth", baseUrl: "https://runtime.us-east-1.kiro.dev", models, + ...(form === "provider" ? { pinnedReasoningEffort: effort } : {}), + ...(form === "model" ? { modelPinnedReasoningEfforts: { [models[0]!]: effort } } : {}), + } } } as unknown as OcxConfig; +} + +/** Capture the wire body; answer 500 so no case depends on a synthetic success frame. */ +function captureWire(): { bodies: Record[] } { + const bodies: Record[] = []; + globalThis.fetch = (async (_input: RequestInfo | URL, init?: RequestInit) => { + bodies.push(JSON.parse(String(init?.body)) as Record); + return new Response("{}", { status: 500, headers: { "content-type": "application/json" } }); + }) as typeof fetch; + return { bodies }; +} + +async function responsesRequest( + model: string, effort: string | undefined, pin: Pin | undefined, stream = false, +) { + const captured = captureWire(); + const response = await handleResponses(new Request("http://localhost/v1/responses", { + method: "POST", headers: { "content-type": "application/json" }, + body: JSON.stringify({ + model: `kiro/${model}`, input: "hi", stream, + ...(effort ? { reasoning: { effort } } : {}), + }), + }), config([model], pin), { model, provider: "kiro" }); + const body = await response.json() as { + error?: { message?: string; type?: string; code?: string }; + }; + return { + status: response.status, error: body.error, wires: captured.bodies, + contentType: response.headers.get("content-type"), + }; +} + +/** + * The Messages surface. Its public contract for a local build refusal is the fixed condition copy, + * not the adapter's detail, so this returns the raw body for an exact-shape assertion rather than + * digging out `error.message` and hiding an extra field. + */ +async function messagesRequest(model: string, pin: Pin | undefined, stream = false) { + const captured = captureWire(); + const response = await handleClaudeMessages(new Request("http://localhost/v1/messages", { + method: "POST", + headers: { "content-type": "application/json", "anthropic-version": "2023-06-01" }, + body: JSON.stringify({ + model: `kiro/${model}`, max_tokens: 256, stream, + messages: [{ role: "user", content: "hi" }], + }), + }), config([model], pin), { model: "", provider: "" } as never); + const body = await response.json() as unknown; + return { + status: response.status, body, wires: captured.bodies, + contentType: response.headers.get("content-type"), + }; +} + +/** Chat Completions is the third surface that reaches this adapter, and its copy is fixed too. */ +async function chatRequest(model: string, pin: Pin | undefined, stream = false) { + const captured = captureWire(); + const response = await handleChatCompletions(new Request("http://localhost/v1/chat/completions", { + method: "POST", headers: { "content-type": "application/json" }, + body: JSON.stringify({ + model: `kiro/${model}`, stream, messages: [{ role: "user", content: "hi" }], + }), + }), config([model], pin), { model: "", provider: "" } as never); + const body = await response.json() as unknown; + return { + status: response.status, body, wires: captured.bodies, + contentType: response.headers.get("content-type"), + }; +} + +const reasoningField = (wire: Record | undefined) => + wire?.additionalModelRequestFields as Record | undefined; +const userContent = (wire: Record | undefined) => (wire?.conversationState as { + currentMessage: { userInputMessage: { content: string } }; +} | undefined)?.currentMessage.userInputMessage.content; + +// Pins are applied after parsing. Exercise each form through every client surface, including +// the seven Claude ids newly moved from emulated to native effort. +const affectedModels = [ + "claude-opus-4.8", "claude-opus-4.7", "claude-opus-4.6", "claude-sonnet-5.5", + "claude-sonnet-5", "claude-sonnet-4.6", "claude-haiku-5.5", +]; +for (const model of affectedModels) { + for (const form of ["provider", "model", "global"] as const) { + for (const surface of ["responses", "chat", "messages"] as const) { + test(`${surface}: ${model} ${form} ultra pin sends native max`, async () => { + const pin = { form, effort: "ultra" }; + for (const stream of [false, true]) { + const result = surface === "responses" ? await responsesRequest(model, "high", pin, stream) + : surface === "chat" ? await chatRequest(model, pin, stream) + : await messagesRequest(model, pin, stream); + expect(result.status).toBe(surface === "messages" ? 529 : 500); + expect(result.wires).toHaveLength(1); + expect(reasoningField(result.wires[0])).toEqual({ output_config: { effort: "max" } }); + } + }); + } + } +} + +test("native GPT ultra pin also sends max", async () => { + const result = await responsesRequest("gpt-5.6-sol", undefined, "ultra"); + expect(result.status).toBe(500); + expect(result.wires).toHaveLength(1); + expect(reasoningField(result.wires[0])).toEqual({ reasoning: { effort: "max" } }); +}); + +test("Chat Completions retains fixed refusal copy for minimal", async () => { + for (const stream of [false, true]) { + const { status, body, wires, contentType } = await chatRequest("claude-opus-5.5", "minimal", stream); + expect(status).toBe(400); + expect(body).toEqual({ error: { message: "The provider rejected the request.", + type: "invalid_request_error", param: null, code: "invalid_request_error" } }); + expect(wires).toHaveLength(0); + expect(contentType).toContain("application/json"); + } +}); + +test("pinned minimal is refused with the same Responses detail on both stream modes", async () => { + // The emulated set admits `minimal`; the rest of the native table rejects it. These ids are the + // ones the docs name as refusing, so the roster here is spelled out instead of derived from the + // allowlist under test. + for (const model of ["claude-opus-5", "claude-opus-5.5", "gpt-5.6-sol"]) { + for (const stream of [false, true]) { + const { status, error, wires } = await responsesRequest(model, undefined, "minimal", stream); + expect({ model, stream, status }).toEqual({ model, stream, status: 400 }); + expect({ model, stream, message: error?.message }).toEqual({ + model, stream, message: `Kiro ${model} does not support reasoning effort "minimal"`, + }); + expect({ model, stream, sends: wires.length }).toEqual({ model, stream, sends: 0 }); + } + } +}); + +test("a model with no native field keeps its emulated path, so a pinned ultra is dropped silently", async () => { + // `claude-sonnet-4.5` never takes the native field, so the adapter's ladder check never runs and + // `kiroThinkingBudget` has no `ultra` entry: the request is sent with no reasoning signal at all. + // This is pre-existing behavior for emulated models, not a consequence of the native Claude work, + // and it is why the docs must say "dropped" rather than "rejected" for this half of the roster. + const dropped = await responsesRequest("claude-sonnet-4.5", "high", "ultra"); + expect(dropped.status).toBe(500); + expect(dropped.wires).toHaveLength(1); + expect(reasoningField(dropped.wires[0])).toBeUndefined(); + expect(userContent(dropped.wires[0])).not.toContain(""); + + // Same model, same path, a pin the emulation does understand: the pin demonstrably arrives. + const minimal = await responsesRequest("claude-sonnet-4.5", "high", "minimal"); + expect(minimal.status).toBe(500); + expect(minimal.wires).toHaveLength(1); + expect(reasoningField(minimal.wires[0])).toBeUndefined(); + expect(userContent(minimal.wires[0])).toContain("enabled"); +}); + +test("the Messages surface replaces the adapter detail with the fixed condition copy", async () => { + // The same throw reaches a different public body here: `ownedAnthropicErrorPayload` re-shapes the + // Responses error and keeps only `upstreamConditionMessage("request_rejected")`, so the model and + // effort never reach a Messages client. Asserting the whole body pins that replacement rather + // than a substring a future forwarding change would still satisfy. + for (const [model, pin] of [["claude-opus-5", "minimal"]] as const) { + for (const stream of [false, true]) { + const { status, body, wires, contentType } = await messagesRequest(model, pin, stream); + expect({ model, pin, stream, status }).toEqual({ model, pin, stream, status: 400 }); + expect({ model, pin, stream, body }).toEqual({ + model, pin, stream, + body: { + type: "error", + error: { type: "invalid_request_error", message: "The provider rejected the request." }, + }, + }); + expect({ model, pin, stream, sends: wires.length }).toEqual({ model, pin, stream, sends: 0 }); + expect(contentType).toContain("application/json"); + } + } +}); + +test("a caller's own ultra is folded on the Messages surface too, before the adapter", async () => { + // `src/claude/inbound.ts` copies `output_config.effort` verbatim (its set includes `ultra`), so + // what reduces the value is the Responses parser the translated request is handed to — not the + // translator. Both surfaces therefore fold a caller's `ultra` and neither can deliver it. + // A synthetic upstream 500 arrives as 529 `overloaded_error` here: claude-messages reclassifies + // transient upstream 5xx so the Claude Code client backs off instead of failing fatally. + const captured = captureWire(); + const response = await handleClaudeMessages(new Request("http://localhost/v1/messages", { + method: "POST", + headers: { "content-type": "application/json", "anthropic-version": "2023-06-01" }, + body: JSON.stringify({ + model: "kiro/claude-opus-4.8", max_tokens: 256, + output_config: { effort: "ultra" }, + messages: [{ role: "user", content: "hi" }], + }), + }), config(["claude-opus-4.8"]), { model: "", provider: "" } as never); + expect(response.status).toBe(529); + expect(captured.bodies).toHaveLength(1); + expect(reasoningField(captured.bodies[0])).toEqual({ output_config: { effort: "max" } }); +}); diff --git a/tests/providers/kiro/kiro-reasoning-roundtrip.test.ts b/tests/providers/kiro/kiro-reasoning-roundtrip.test.ts index 7eb45b4077b..28c53fc8209 100644 --- a/tests/providers/kiro/kiro-reasoning-roundtrip.test.ts +++ b/tests/providers/kiro/kiro-reasoning-roundtrip.test.ts @@ -300,3 +300,52 @@ describe("kiro native reasoning effort — the GPT-5.6 family", () => { .toEqual({ output_config: { effort: "xhigh" } }); }); }); + +// Live probe on 2026-11-07: Kiro accepted output_config.effort at every rung for these Claude +// models except xhigh on the 4.6 pair, and rejected it (HTTP 400) for Haiku 4.5, Opus 4.5 and Sonnet 4.5. +describe("kiro native reasoning effort — probed Claude models", () => { + function wireBody(modelId: string, effort: string): Record { + const parsed = { + modelId, + stream: true, + options: { reasoning: effort, maxOutputTokens: 1000 }, + context: { messages: [{ role: "user", content: "solve" }] }, + } as unknown as Parameters[0]; + return buildKiroPayload(parsed, undefined, "disabled", "ide").payload; + } + const content = (body: Record) => (body.conversationState as { + currentMessage: { userInputMessage: { content: string } }; + }).currentMessage.userInputMessage.content; + const RUNGS = ["low", "medium", "high", "xhigh", "max"]; + const NATIVE: Record = { + "claude-opus-5.5": RUNGS, "claude-opus-5": RUNGS, "claude-opus-4.8": RUNGS, "claude-opus-4.7": RUNGS, + "claude-sonnet-5.5": RUNGS, "claude-sonnet-5": RUNGS, "claude-haiku-5.5": RUNGS, + "claude-opus-4.6": ["low", "medium", "high", "max"], "claude-sonnet-4.6": ["low", "medium", "high", "max"], + }; + + for (const [modelId, efforts] of Object.entries(NATIVE)) { + test(`${modelId} sends its accepted rungs natively, with no thinking tags`, () => { + for (const effort of efforts) { + const body = wireBody(modelId, effort); + expect(body.additionalModelRequestFields).toEqual({ output_config: { effort } }); + expect(content(body)).toBe("solve"); + } + }); + } + + test("xhigh on the 4.6 pair stays emulated", () => { + for (const modelId of ["claude-opus-4.6", "claude-sonnet-4.6"]) { + const body = wireBody(modelId, "xhigh"); + expect(body.additionalModelRequestFields).toBeUndefined(); + expect(content(body)).toContain("enabled"); + } + }); + + test("models that rejected the native field stay emulated", () => { + for (const modelId of ["claude-haiku-4.5", "claude-opus-4.5", "claude-sonnet-4.5"]) { + const body = wireBody(modelId, "high"); + expect(body.additionalModelRequestFields).toBeUndefined(); + expect(content(body)).toContain("enabled"); + } + }); +}); diff --git a/tests/providers/kiro/kiro-structured-output.test.ts b/tests/providers/kiro/kiro-structured-output.test.ts index 970b4d78044..474d0f3d438 100644 --- a/tests/providers/kiro/kiro-structured-output.test.ts +++ b/tests/providers/kiro/kiro-structured-output.test.ts @@ -116,11 +116,11 @@ describe("Kiro structured-output payload", () => { const { current, tools, names } = await payloadOf(parsed); // The structured tool REPLACES the completion tool: both would offer the model a prose exit. expect(names).toEqual([KIRO_STRUCTURED_OUTPUT_TOOL_NAME]); - // Sanitized the way every Kiro tool schema is: additionalProperties is a validator reject. + // The 5.5 policy strips unproven additionalProperties while retaining the caller schema in prose. expect(tools[0].toolSpecification.inputSchema.json).toEqual({ type: "object", properties: { title: { type: "string" } }, required: ["title"], }); - // ...so the caller's original constraints are restated verbatim in the prose contract. + // The caller's original constraints are also restated verbatim in the prose contract. expect(current.content).toContain(`calling ${KIRO_STRUCTURED_OUTPUT_TOOL_NAME} exactly once`); expect(current.content).toContain(JSON.stringify(titleSchema)); // A tool-free turn gets no work-tool catalog nudge and no completion contract. @@ -136,18 +136,75 @@ describe("Kiro structured-output payload", () => { expect(current.content).not.toContain(KIRO_COMPLETION_TOOL_NAME); }); + test("a non-Claude model gets the structured schema without the keywords Kiro rejects", async () => { + const { tools } = await payloadOf(parseRequest({ + model: "kiro/gpt-5.6-sol", + input: "Title this thread: fix the Kiro structured output bug", + stream: true, + text: { format: { type: "json_schema", name: "response", schema: titleSchema } }, + } as never)); + expect(tools[0].toolSpecification.inputSchema.json).toEqual({ + type: "object", properties: { title: { type: "string" } }, required: ["title"], + }); + }); + + test("the structured tool follows the per-id keyword measurement, not a Claude-wide rule", async () => { + // `maxLength` was measured on opus-4.5 only. On opus-5.5 the same keyword must still be + // stripped, or an unprobed keyword becomes a whole-request ValidationException. + const schema = { + type: "object", + properties: { title: { type: "string", maxLength: 80, pattern: "^[a-z ]+$" } }, + required: ["title"], + }; + const format = { type: "json_schema", name: "title", schema }; + const wide = await payloadOf(parseRequest({ + model: "kiro/claude-opus-4.5", + input: "Title this thread", + stream: true, + text: { format }, + } as never)); + expect(wide.tools[0].toolSpecification.inputSchema.json.properties.title) + .toEqual({ type: "string", maxLength: 80, pattern: "^[a-z ]+$" }); + + const narrow = await payloadOf(parseRequest({ + model: "kiro/claude-opus-5.5", + input: "Title this thread", + stream: true, + text: { format }, + } as never)); + expect(narrow.tools[0].toolSpecification.inputSchema.json.properties.title) + .toEqual({ type: "string", pattern: "^[a-z ]+$" }); + // The caller's stripped `maxLength` still reaches the model through the prose restatement. + expect(narrow.current.content).toContain(JSON.stringify(schema)); + }); + test("a non-object or composed root is wrapped under `value`, with definitions hoisted", async () => { const schema = { anyOf: [{ $ref: "#/$defs/item" }, { type: "array", items: { $ref: "#/$defs/item" } }], $defs: { item: { type: "string", maxLength: 10 } }, }; - const { tools } = await payloadOf(structuredRequest({ type: "json_schema", name: "items", schema })); - expect(tools[0].toolSpecification.inputSchema.json).toEqual({ + const wrapped = (item: Record) => ({ type: "object", properties: { value: { anyOf: [{ $ref: "#/$defs/item" }, { type: "array", items: { $ref: "#/$defs/item" } }] } }, required: ["value"], - $defs: { item: { type: "string" } }, + $defs: { item }, }); + // opus-4.5, because the wrapped `maxLength` only survives where its probe measured it. + const claude = await payloadOf(parseRequest({ + model: "kiro/claude-opus-4.5", + input: "Title this thread: fix the Kiro structured output bug", + stream: true, + text: { format: { type: "json_schema", name: "items", schema } }, + } as never)); + expect(claude.tools[0].toolSpecification.inputSchema.json).toEqual(wrapped({ type: "string", maxLength: 10 })); + + const gpt = await payloadOf(parseRequest({ + model: "kiro/gpt-5.6-sol", + input: "Title this thread: fix the Kiro structured output bug", + stream: true, + text: { format: { type: "json_schema", name: "items", schema } }, + } as never)); + expect(gpt.tools[0].toolSpecification.inputSchema.json).toEqual(wrapped({ type: "string" })); }); test("json_object asks for any JSON object", async () => { diff --git a/tests/providers/kiro/kiro-tool-catalog.test.ts b/tests/providers/kiro/kiro-tool-catalog.test.ts new file mode 100644 index 00000000000..e239307ff49 --- /dev/null +++ b/tests/providers/kiro/kiro-tool-catalog.test.ts @@ -0,0 +1,267 @@ +import { afterEach, beforeEach, describe, expect, test } from "bun:test"; +import { mkdtempSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { createKiroAdapter } from "../../../src/adapters/kiro"; +import { KIRO_TOOL_BUDGET_ENV, KIRO_TOOL_DESCRIPTION_LIMIT_ENV, MAX_KIRO_TOOL_CATALOG_BYTES, MAX_KIRO_TOOL_COUNT } from "../../../src/adapters/kiro-tools"; +import type { OcxParsedRequest, OcxProviderConfig } from "../../../src/types"; +import { removeTreeWithRetry } from "../../helpers/remove-tree"; + +// The catalog budget, the description cap, the Anthropic-surface nudge and the per-family schema +// policy all shape the tools Kiro receives. They live apart from kiro-adapter.test.ts so that file +// stays under its file-size cap; the isolation below still mirrors it, because buildRequest reads +// the native kiro-cli store from HOME and from the platform-specific profile roots. +const origHome = process.env.HOME; +const origLocalAppData = process.env.LOCALAPPDATA; +const origUserProfile = process.env.USERPROFILE; +const origRegion = process.env.KIRO_REGION; +const origOcxHome = process.env.OPENCODEX_HOME; +let tmp: string; + +beforeEach(() => { + tmp = mkdtempSync(join(tmpdir(), "kiro-tool-catalog-")); + process.env.HOME = tmp; + process.env.LOCALAPPDATA = join(tmp, "AppData", "Local"); + process.env.USERPROFILE = tmp; + process.env.OPENCODEX_HOME = tmp; + process.env.KIRO_REGION = "us-east-1"; +}); +afterEach(() => { + if (origHome === undefined) delete process.env.HOME; else process.env.HOME = origHome; + if (origLocalAppData === undefined) delete process.env.LOCALAPPDATA; else process.env.LOCALAPPDATA = origLocalAppData; + if (origUserProfile === undefined) delete process.env.USERPROFILE; else process.env.USERPROFILE = origUserProfile; + if (origRegion === undefined) delete process.env.KIRO_REGION; else process.env.KIRO_REGION = origRegion; + if (origOcxHome === undefined) delete process.env.OPENCODEX_HOME; else process.env.OPENCODEX_HOME = origOcxHome; + removeTreeWithRetry(tmp); +}); + +const provider = { adapter: "kiro", baseUrl: "https://runtime.us-east-1.kiro.dev", authMode: "oauth", apiKey: "tok-123" } as unknown as OcxProviderConfig; +const bashTool = { name: "bash", description: "Run a shell command", parameters: { type: "object" } }; + +function parsedWith(messages: unknown[], tools?: unknown[], modelId = "claude-sonnet-4.5"): OcxParsedRequest { + return { modelId, stream: true, options: {}, context: { messages, tools } } as unknown as OcxParsedRequest; +} + +describe("kiro catalog budget opt-out", () => { + const origBudget = process.env[KIRO_TOOL_BUDGET_ENV]; + afterEach(() => { + if (origBudget === undefined) delete process.env[KIRO_TOOL_BUDGET_ENV]; + else process.env[KIRO_TOOL_BUDGET_ENV] = origBudget; + }); + + // Claude Desktop declares its own tools last, so a budgeted catalog drops them first. + const catalog = [ + ...Array.from({ length: MAX_KIRO_TOOL_COUNT + 60 }, (_, index) => ({ + name: `mcp__filler__tool_${String(index).padStart(3, "0")}`, + description: `Filler ${index} ${"x".repeat(900)}`, + parameters: { type: "object", properties: { value: { type: "string", description: "y".repeat(200) } } }, + })), + { name: "mcp__ccd_turn__update_plan", description: "Update the plan", parameters: { type: "object" } }, + { name: "mcp__ccd_turn__wrap_up", description: "Wrap up the turn", parameters: { type: "object" } }, + ]; + + async function emitted(): Promise<{ names: string[]; content: string }> { + const { body } = await createKiroAdapter(provider).buildRequest(parsedWith([{ role: "user", content: "hi" }], catalog)); + const current = JSON.parse(body).conversationState.currentMessage.userInputMessage; + return { + names: current.userInputMessageContext.tools.slice(0, -1).map((tool: { toolSpecification: { name: string } }) => tool.toolSpecification.name), + content: current.content, + }; + } + + test("default keeps both budgets and drops the last-declared tools", async () => { + delete process.env[KIRO_TOOL_BUDGET_ENV]; + const { names, content } = await emitted(); + + expect(names.length).toBeLessThanOrEqual(MAX_KIRO_TOOL_COUNT); + expect(names).not.toContain("mcp__ccd_turn__wrap_up"); + expect(content).toContain("Kiro's outbound catalog budget allows"); + }); + + test("off sends the whole catalog in declared order without a notice", async () => { + process.env[KIRO_TOOL_BUDGET_ENV] = "off"; + const { names, content } = await emitted(); + + expect(new TextEncoder().encode(JSON.stringify(catalog)).byteLength).toBeGreaterThan(MAX_KIRO_TOOL_CATALOG_BYTES); + expect(names).toEqual(catalog.map(tool => tool.name)); + expect(content).not.toContain("Kiro's outbound catalog budget allows"); + }); + + test("any other value keeps the budget", async () => { + process.env[KIRO_TOOL_BUDGET_ENV] = "0"; + const { names } = await emitted(); + + expect(names.length).toBeLessThanOrEqual(MAX_KIRO_TOOL_COUNT); + }); +}); + +describe("kiro tool description limit opt-out", () => { + const origLimit = process.env[KIRO_TOOL_DESCRIPTION_LIMIT_ENV]; + afterEach(() => { + if (origLimit === undefined) delete process.env[KIRO_TOOL_DESCRIPTION_LIMIT_ENV]; + else process.env[KIRO_TOOL_DESCRIPTION_LIMIT_ENV] = origLimit; + }); + + const longDescription = `Long docs ${"x".repeat(5_000)} keep this tail.`; + + async function emittedDescription(modelId?: string): Promise { + const { body } = await createKiroAdapter(provider).buildRequest( + parsedWith([{ role: "user", content: "hi" }], [{ name: "longtool", description: longDescription, parameters: { type: "object" } }], modelId), + ); + return JSON.parse(body).conversationState.currentMessage.userInputMessage.userInputMessageContext.tools[0].toolSpecification.description; + } + + test("default keeps the 1024-character cap", async () => { + delete process.env[KIRO_TOOL_DESCRIPTION_LIMIT_ENV]; + + expect(await emittedDescription()).toHaveLength(1024); + }); + + test("off sends every description whole, whatever the model", async () => { + process.env[KIRO_TOOL_DESCRIPTION_LIMIT_ENV] = "off"; + + expect(await emittedDescription()).toBe(longDescription); + expect(await emittedDescription("gpt-5.6-sol")).toBe(longDescription); + }); + + test("any other value keeps the cap", async () => { + process.env[KIRO_TOOL_DESCRIPTION_LIMIT_ENV] = "0"; + + expect(await emittedDescription()).toHaveLength(1024); + }); +}); + +describe("kiro tool-catalog nudge and the Anthropic Messages surface", () => { + // Claude Code and Claude Desktop send their own tool rules with the full catalog; the + // Codex-oriented nudge (neighbour-tool bans, code-mode guidance) only repeats it. + async function currentContent(anthropicInbound: boolean, modelId?: string): Promise { + const parsed = parsedWith([{ role: "user", content: "hi" }], [bashTool], modelId); + if (anthropicInbound) parsed._anthropicInbound = true; + const { body } = await createKiroAdapter(provider).buildRequest(parsed); + return JSON.parse(body).conversationState.currentMessage.userInputMessage.content; + } + + test("a Codex request keeps the nudge", async () => { + expect(await currentContent(false)).toContain("Tool contract: use the current tool catalog as ground truth."); + }); + + test("a Claude request drops the nudge and keeps the completion contract", async () => { + const content = await currentContent(true); + + expect(content).not.toContain("Tool contract:"); + expect(content).not.toContain("neighboring-agent tool names"); + expect(content).toContain("call codex_kiro_final_answer exactly once"); + }); + + test("a non-Claude model on the Messages surface drops the nudge too", async () => { + // The nudge is scoped to the inbound surface, not the routed model: it names Codex-only + // concepts (code mode, neighbour-tool bans), and every Messages client ships its own tool + // rules. A model-conditional rule would hand a routed DeepSeek or MiniMax turn guidance about + // a harness whose tools that turn does not have. + const content = await currentContent(true, "deepseek-3.2"); + + expect(content).not.toContain("Tool contract:"); + expect(content).toContain("call codex_kiro_final_answer exactly once"); + // The Codex surface still carries it for that same model. + expect(await currentContent(false, "deepseek-3.2")).toContain("Tool contract:"); + }); +}); + +describe("kiro tool schemas by model family", () => { + // PR #8451 reports individual opus-4.5 keyword probes and names pattern and maximum + // in a constrained-tool probe for the two 5.5 models. Other keys remain stripped. + const constrained = { + $schema: "http://json-schema.org/draft-07/schema#", + type: "object", + additionalProperties: false, + properties: { + city: { type: "string", pattern: "^[A-Z][a-z]+$", maxLength: 20, format: "hostname" }, + days: { type: "integer", minimum: 1, maximum: 7 }, + tags: { type: "array", items: { type: "string" }, maxItems: 3, uniqueItems: true }, + }, + required: ["city", "days"], + }; + // The same schema after the two 5.5 ids' measured keywords survive and the rest are stripped. + const constrainedMeasuredOn55 = { + type: "object", + properties: { + city: { type: "string", pattern: "^[A-Z][a-z]+$" }, + days: { type: "integer", maximum: 7 }, + tags: { type: "array", items: { type: "string" } }, + }, + required: ["city", "days"], + }; + const stripped = { + type: "object", + properties: { + city: { type: "string" }, + days: { type: "integer" }, + tags: { type: "array", items: { type: "string" } }, + }, + required: ["city", "days"], + }; + + async function emittedSchema(modelId: string): Promise> { + const { body } = await createKiroAdapter(provider).buildRequest( + parsedWith([{ role: "user", content: "hi" }], [{ name: "forecast", description: "Forecast", parameters: constrained }], modelId), + ); + return JSON.parse(body).conversationState.currentMessage.userInputMessage.userInputMessageContext.tools[0].toolSpecification.inputSchema.json; + } + + test("claude-opus-4.5 receives the schema with every keyword, the id probed one per request", async () => { + expect(await emittedSchema("claude-opus-4.5")).toEqual(constrained); + }); + + test("the two 5.5 ids keep only the keywords their probe covered", async () => { + // The PR only names two tested keywords for these ids; other constraints stay stripped. + for (const modelId of ["claude-opus-5.5", "claude-sonnet-5.5"]) { + expect(await emittedSchema(modelId)).toEqual(constrainedMeasuredOn55); + } + }); + + test("an unmeasured Claude model keeps the strip instead of inheriting the measurement", async () => { + // Haiku 5.5 is in KIRO_MODELS and shares the Claude wire, but the PR reports no keyword probe for it. + // Guessing wide turns an unmeasured keyword into an upstream ValidationException. + for (const modelId of ["claude-haiku-5.5", "claude-sonnet-4.5", "claude-opus-4.8"]) { + expect(await emittedSchema(modelId)).toEqual(stripped); + } + }); + + test("a non-Claude model still receives the stripped schema", async () => { + const schema = await emittedSchema("gpt-5.6-sol"); + expect(schema.$schema).toBeUndefined(); + expect(schema.additionalProperties).toBeUndefined(); + expect(schema.properties.city).toEqual({ type: "string" }); + expect(schema.properties.days).toEqual({ type: "integer" }); + expect(schema.properties.tags).toEqual({ type: "array", items: { type: "string" } }); + expect(schema.required).toEqual(["city", "days"]); + }); + + test("a Claude root composition is still flattened to an object", async () => { + const { body } = await createKiroAdapter(provider).buildRequest( + parsedWith([{ role: "user", content: "hi" }], [{ + name: "either", description: "Either", parameters: { oneOf: [{ type: "object", properties: { a: { type: "string", maxLength: 3 } } }] }, + // opus-4.5, because `maxLength` is only measured to survive on that id: this case checks + // flattening, not the keyword exception. + }], "claude-opus-4.5"), + ); + const schema = JSON.parse(body).conversationState.currentMessage.userInputMessage.userInputMessageContext.tools[0].toolSpecification.inputSchema.json; + expect(schema.type).toBe("object"); + expect(schema.oneOf).toBeUndefined(); + expect(schema.properties.a).toEqual({ type: "string", maxLength: 3 }); + }); +}); + +describe("Kiro effort fallback", () => { + test("the minimal fallback is that one value, not any effort the ladder does not know", async () => { + // The legacy outside-ladder value is `minimal`; `ultra` is folded to `max`. If the + // fallback keyed on "not in KIRO_NATIVE_EFFORTS" instead of the value, a future rung would + // silently lose reasoning here instead of failing where the operator can see it. + for (const modelId of ["claude-opus-4.8", "claude-sonnet-5.5"]) { + await expect(createKiroAdapter(provider).buildRequest({ + ...parsedWith([{ role: "user", content: "solve" }], undefined, modelId), + options: { reasoning: "unheard-of" }, + })).rejects.toThrow(`Kiro ${modelId} does not support reasoning effort "unheard-of"`); + } + }); +}); diff --git a/tests/server/server-kiro-structured-output-e2e.test.ts b/tests/server/server-kiro-structured-output-e2e.test.ts index 7421290f1d5..0712b44e988 100644 --- a/tests/server/server-kiro-structured-output-e2e.test.ts +++ b/tests/server/server-kiro-structured-output-e2e.test.ts @@ -253,3 +253,44 @@ describe("Kiro holds a Messages schema to full conformance", () => { }); }); }); + +describe("Kiro tool-catalog nudge by inbound surface", () => { + const completionFrames = (answer: string): Uint8Array[] => [ + eventFrame("toolUseEvent", { name: KIRO_COMPLETION_TOOL_NAME, toolUseId: "done-1" }), + eventFrame("toolUseEvent", { name: KIRO_COMPLETION_TOOL_NAME, toolUseId: "done-1", input: JSON.stringify({ answer }) }), + eventFrame("toolUseEvent", { name: KIRO_COMPLETION_TOOL_NAME, toolUseId: "done-1", stop: true }), + ]; + const sentContent = (request: Record): string => + request.conversationState.currentMessage.userInputMessage.content as string; + const lookupTool = { name: "lookup", description: "Look something up", input_schema: { type: "object", properties: { q: { type: "string" } } } }; + + test("/v1/messages reaches Kiro without the nudge but with the completion contract", async () => { + await withProxy([completionFrames("Ready.")], async (url, requests) => { + const response = await post(url, "/v1/messages", { + model: "kiro-test/claude-opus-5.5", + max_tokens: 1024, + tools: [lookupTool], + messages: [{ role: "user", content: "Say ready." }], + }); + expect(response.status, await response.text()).toBe(200); + expect(requests).toHaveLength(1); + expect(advertisedTools(requests[0]!)).toEqual(["lookup", KIRO_COMPLETION_TOOL_NAME]); + expect(sentContent(requests[0]!)).not.toContain("Tool contract:"); + expect(sentContent(requests[0]!)).toContain(`call ${KIRO_COMPLETION_TOOL_NAME} exactly once`); + }); + }); + + test("/v1/responses keeps the nudge for Codex", async () => { + await withProxy([completionFrames("Ready.")], async (url, requests) => { + const response = await post(url, "/v1/responses", { + model: "kiro-test/claude-opus-5.5", + stream: false, + tools: [{ type: "function", name: "lookup", description: "Look something up", parameters: { type: "object", properties: { q: { type: "string" } } } }], + input: [{ role: "user", content: "Say ready." }], + }); + expect(response.status, await response.text()).toBe(200); + expect(requests).toHaveLength(1); + expect(sentContent(requests[0]!)).toContain("Tool contract: use the current tool catalog as ground truth."); + }); + }); +});