From 4931e858031ee7696cd3340bc9cbd56aeac5b6a7 Mon Sep 17 00:00:00 2001 From: "wentao.ma2" Date: Tue, 15 Sep 2026 11:28:35 +0800 Subject: [PATCH 1/3] fix(kiro): send native reasoning effort for the GPT-5.6 family and replay its blob on the right field MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `gpt-5.6-luna` and `gpt-5.6-terra` were missing from `KIRO_NATIVE_EFFORT_FIELDS`, so a request asking for `low`/`medium`/`high`/`max` reached Kiro with the emulated `` prompt and no `additionalModelRequestFields.reasoning.effort` at all. Both models accept the native field on the live runtime. The encrypted reasoning blob those models return also arrives on `reasoningContentEvent.signature`, not `redactedContent`, and its `.KTR~~…` value is not base64. The adapter read `redactedContent` only — a member none of the thirteen captures sent (all thirteen carried `{signature, text}`) — so the blob was dropped and the next turn had no previous reasoning to replay; sending that value on `redactedContent` instead comes back HTTP 400 `REQUEST_BODY_INVALID` ("Improperly formed request"). The blob now carries the field it arrived on (a `signature:` tag) from the adapter event through the `ocxr1:` envelope to `assistantResponseMessage.reasoningContent`, and is replayed verbatim on that member. Provider data cannot forge the tag: the other member is base64, whose alphabet has no colon. Measured on the live runtime against one fixed hard prompt, HTTP 200 throughout: - luna's reasoning blob 5,130 chars at native `low`, 16,686 at `medium`, 30,670 at `high` and 48,594 at `max`; a bare prompt with no effort signal returned 13,118, and `gpt-5.6-sol`'s native `max` cross-checked at 30,498. - The emulated tag channel that used to serve these models: 21,202 (`low`) and 28,302 (`max`) — between native `medium` and `high`, never reaching native `max`. - terra, two repetitions each: 11,758 / 17,598 bare against 34,590 / 38,106 at native `max`. - Replay A/B on one captured luna blob: `{signature: …}` 200, `{redactedContent: …}` 400 `com.amazon.kiro.runtimeservice#ValidationException / REQUEST_BODY_INVALID`. The new assertions live in `tests/providers/kiro/kiro-reasoning-roundtrip.test.ts`, next to the round-trip they belong to, because `kiro-adapter.test.ts` and `kiro-stream.test.ts` both sit at their file-size-ratchet cap and a baselined file may not grow by a single line (`tests/fixtures/file-size-baseline.json`). `kiro-adapter.test.ts` still extends its existing unsupported-effort loop to luna and terra, which rewrites one line and leaves the cap intact. Verification: - `bun run typecheck` - `bun test tests/providers/kiro` — 439 pass / 0 fail - `bun test tests/ci-workflows/file-size-ratchet.test.ts` — 6 pass / 0 fail - `bun run structure:check`, `bun run privacy:scan` --- .../src/content/docs/fr/reference/adapters.md | 2 +- .../src/content/docs/ja/reference/adapters.md | 6 +- .../src/content/docs/ko/reference/adapters.md | 6 +- .../src/content/docs/reference/adapters.md | 7 +- .../src/content/docs/ru/reference/adapters.md | 4 +- .../src/content/docs/tr/reference/adapters.md | 4 +- .../content/docs/zh-cn/reference/adapters.md | 6 +- .../content/docs/zh-tw/reference/adapters.md | 6 +- src/adapters/kiro-events.ts | 34 +++-- src/adapters/kiro/payload.ts | 13 +- src/adapters/kiro/reasoning.ts | 57 ++++++- src/adapters/kiro/stream.ts | 10 +- src/adapters/kiro/wire.ts | 3 +- src/providers/kiro-models.ts | 7 +- src/responses/reasoning-envelope.ts | 9 +- src/types/request.ts | 13 +- structure/providers/kiro.md | 36 +++-- tests/providers/kiro/kiro-adapter.test.ts | 2 +- .../kiro/kiro-reasoning-roundtrip.test.ts | 142 ++++++++++++++++++ 19 files changed, 303 insertions(+), 64 deletions(-) diff --git a/docs-site/src/content/docs/fr/reference/adapters.md b/docs-site/src/content/docs/fr/reference/adapters.md index 04ff6ccd443..dc28832ca7e 100644 --- a/docs-site/src/content/docs/fr/reference/adapters.md +++ b/docs-site/src/content/docs/fr/reference/adapters.md @@ -131,7 +131,7 @@ Si Kiro s’arrête sans appeler l’outil d’achèvement, l’adaptateur effec ### Effort de raisonnement -`gpt-5.6-sol` et `claude-opus-5` prennent en charge nativement un niveau d’effort vérifié, mais chaque famille de modèles nomme différemment le champ de la requête. La valeur sélectionnée `low`, `medium`, `high`, `xhigh` ou `max` est envoyée dans `additionalModelRequestFields.reasoning.effort` pour `gpt-5.6-sol`, et dans `additionalModelRequestFields.output_config.effort` pour `claude-opus-5`. Les autres modèles Kiro utilisent actuellement un raisonnement émulé : opencodex convertit le niveau choisi en instructions de réflexion bornées dans le contenu utilisateur, car leur champ d’effort natif n’a pas été vérifié. La présence d’un contrôle d’effort annoncé sur ces modèles ne prouve donc pas la prise en charge native du raisonnement en amont. +La famille GPT-5.6 de Kiro (`gpt-5.6-sol`, `gpt-5.6-terra`, `gpt-5.6-luna`) et `claude-opus-5` prennent en charge nativement un niveau d’effort vérifié, mais chaque famille de modèles nomme différemment le champ de la requête. La valeur sélectionnée `low`, `medium`, `high`, `xhigh` ou `max` est envoyée dans `additionalModelRequestFields.reasoning.effort` pour les modèles GPT-5.6, et dans `additionalModelRequestFields.output_config.effort` pour `claude-opus-5`. Les autres modèles Kiro utilisent actuellement un raisonnement émulé : opencodex convertit le niveau choisi en instructions de réflexion bornées dans le contenu utilisateur, car leur champ d’effort natif n’a pas été vérifié. La présence d’un contrôle d’effort annoncé sur ces modèles ne prouve donc pas la prise en charge native du raisonnement en amont. ## `cursor` diff --git a/docs-site/src/content/docs/ja/reference/adapters.md b/docs-site/src/content/docs/ja/reference/adapters.md index f1276511b1d..2191cd41dd0 100644 --- a/docs-site/src/content/docs/ja/reference/adapters.md +++ b/docs-site/src/content/docs/ja/reference/adapters.md @@ -154,9 +154,9 @@ filtered incomplete になります。実際のツール呼び出しを伴わな ### Reasoning effort -`gpt-5.6-sol` と `claude-opus-5` はネイティブ effort をサポートし、リクエストフィールド名が異なります。 -`low` / `medium` / `high` / `xhigh` / `max` は、前者では -`additionalModelRequestFields.reasoning.effort`、後者では `output_config.effort` として送信されます。 +`gpt-5.6-sol` / `gpt-5.6-terra` / `gpt-5.6-luna` と `claude-opus-5` はネイティブ effort をサポートし、リクエストフィールド名が異なります。 +`low` / `medium` / `high` / `xhigh` / `max` は、GPT-5.6 系では +`additionalModelRequestFields.reasoning.effort`、`claude-opus-5` では `output_config.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 83aeaf2dd3f..4ff2cc0b370 100644 --- a/docs-site/src/content/docs/ko/reference/adapters.md +++ b/docs-site/src/content/docs/ko/reference/adapters.md @@ -167,9 +167,9 @@ commentary로 유지하고 비공개 완료 툴을 한 번 검증합니다. ### Reasoning effort -`gpt-5.6-sol`과 `claude-opus-5`는 네이티브 effort를 지원하며 요청 필드 이름이 다릅니다. -`low` / `medium` / `high` / `xhigh` / `max` 값은 각각 -`additionalModelRequestFields.reasoning.effort`와 `output_config.effort`로 전송됩니다. +`gpt-5.6-sol`, `gpt-5.6-terra`, `gpt-5.6-luna`와 `claude-opus-5`는 네이티브 effort를 지원하며 요청 필드 이름이 다릅니다. +`low` / `medium` / `high` / `xhigh` / `max` 값은 GPT-5.6 계열에서는 +`additionalModelRequestFields.reasoning.effort`, `claude-opus-5`에서는 `output_config.effort`로 전송됩니다. ## `cursor` diff --git a/docs-site/src/content/docs/reference/adapters.md b/docs-site/src/content/docs/reference/adapters.md index 715555f847c..f5e23106240 100644 --- a/docs-site/src/content/docs/reference/adapters.md +++ b/docs-site/src/content/docs/reference/adapters.md @@ -364,9 +364,10 @@ important than cosmetic de-duplication. Tool-free requests retain normal text co ### Reasoning effort -`gpt-5.6-sol` and `claude-opus-5` have verified native effort support, and each model family names -the request field differently. A selected `low`, `medium`, `high`, `xhigh`, or `max` value is sent -as `additionalModelRequestFields.reasoning.effort` for `gpt-5.6-sol` and as +The Kiro GPT-5.6 family (`gpt-5.6-sol`, `gpt-5.6-terra`, `gpt-5.6-luna`) and `claude-opus-5` have +verified native effort support, and each model family names the request field differently. A +selected `low`, `medium`, `high`, `xhigh`, or `max` value is sent as +`additionalModelRequestFields.reasoning.effort` for the GPT-5.6 models and as `additionalModelRequestFields.output_config.effort` for `claude-opus-5`. Other Kiro models currently use emulated reasoning: opencodex converts the selected level into bounded thinking instructions in the user content because their native effort field has not been verified. Do not interpret an diff --git a/docs-site/src/content/docs/ru/reference/adapters.md b/docs-site/src/content/docs/ru/reference/adapters.md index d2d1f53de9a..9961210917f 100644 --- a/docs-site/src/content/docs/ru/reference/adapters.md +++ b/docs-site/src/content/docs/ru/reference/adapters.md @@ -189,9 +189,9 @@ incomplete. `TOOL_USE` без фактического вызова инстру ### Reasoning effort -`gpt-5.6-sol` и `claude-opus-5` поддерживают нативный effort, но называют поле запроса по-разному. +Модели семейства GPT-5.6 (`gpt-5.6-sol`, `gpt-5.6-terra`, `gpt-5.6-luna`) и `claude-opus-5` поддерживают нативный effort, но называют поле запроса по-разному. Значения `low` / `medium` / `high` / `xhigh` / `max` отправляются как -`additionalModelRequestFields.reasoning.effort` и `output_config.effort` соответственно. +`additionalModelRequestFields.reasoning.effort` для моделей GPT-5.6 и `output_config.effort` для `claude-opus-5`. ## `cursor` diff --git a/docs-site/src/content/docs/tr/reference/adapters.md b/docs-site/src/content/docs/tr/reference/adapters.md index 6167bfe6df2..876d050b046 100644 --- a/docs-site/src/content/docs/tr/reference/adapters.md +++ b/docs-site/src/content/docs/tr/reference/adapters.md @@ -268,9 +268,9 @@ tam olarak tekrarlasa bile, çünkü aşama doğruluğu kozmetik tekilleştirmed ### Akıl yürütme çabası -`gpt-5.6-sol` ve `claude-opus-5` doğrulanmış yerel çaba desteğine sahiptir ve +Kiro'nun GPT-5.6 ailesi (`gpt-5.6-sol`, `gpt-5.6-terra`, `gpt-5.6-luna`) ve `claude-opus-5` doğrulanmış yerel çaba desteğine sahiptir ve her model ailesi istek alanını farklı şekilde adlandırır. Seçilen `low`, -`medium`, `high`, `xhigh` veya `max` değeri `gpt-5.6-sol` için +`medium`, `high`, `xhigh` veya `max` değeri GPT-5.6 modelleri için `additionalModelRequestFields.reasoning.effort` olarak ve `claude-opus-5` için `additionalModelRequestFields.output_config.effort` olarak gönderilir. Diğer Kiro modelleri şu anda öykünülmüş akıl yürütme kullanır: opencodex yerel çaba 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 e111743887b..6c68bf35265 100644 --- a/docs-site/src/content/docs/zh-cn/reference/adapters.md +++ b/docs-site/src/content/docs/zh-cn/reference/adapters.md @@ -154,9 +154,9 @@ Kiro 的 assistant 文本本身没有可靠的回合结束标记,但终止的 ### Reasoning effort -`gpt-5.6-sol` 和 `claude-opus-5` 支持原生 effort,且请求字段名不同。`low` / `medium` / `high` / -`xhigh` / `max` 分别通过 `additionalModelRequestFields.reasoning.effort` 和 -`output_config.effort` 发送。 +`gpt-5.6-sol`、`gpt-5.6-terra`、`gpt-5.6-luna` 和 `claude-opus-5` 支持原生 effort,且请求字段名不同。`low` / `medium` / `high` / +`xhigh` / `max` 在 GPT-5.6 系列中通过 `additionalModelRequestFields.reasoning.effort` 发送, +在 `claude-opus-5` 上通过 `output_config.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 c155c8ae517..90cb9f8a3fb 100644 --- a/docs-site/src/content/docs/zh-tw/reference/adapters.md +++ b/docs-site/src/content/docs/zh-tw/reference/adapters.md @@ -145,9 +145,9 @@ Kiro 的 assistant 文字本身沒有可靠的回合結束標記,但終止的 ### Reasoning effort -`gpt-5.6-sol` 和 `claude-opus-5` 支援原生 effort,且請求欄位名不同。`low` / `medium` / `high` / -`xhigh` / `max` 分別透過 `additionalModelRequestFields.reasoning.effort` 和 -`output_config.effort` 傳送。 +`gpt-5.6-sol`、`gpt-5.6-terra`、`gpt-5.6-luna` 和 `claude-opus-5` 支援原生 effort,且請求欄位名不同。`low` / `medium` / `high` / +`xhigh` / `max` 在 GPT-5.6 系列中透過 `additionalModelRequestFields.reasoning.effort` 傳送, +在 `claude-opus-5` 上透過 `output_config.effort` 傳送。 ## `cursor` diff --git a/src/adapters/kiro-events.ts b/src/adapters/kiro-events.ts index 3662d8caaab..730ad2a2c4e 100644 --- a/src/adapters/kiro-events.ts +++ b/src/adapters/kiro-events.ts @@ -3,7 +3,7 @@ import { kiroTruncationReason } from "./kiro-truncation"; export type ParsedKiroEvent = | { type: "content"; data?: string; modelId?: string } - | { type: "reasoning"; data?: string; redactedContent?: string } + | { type: "reasoning"; data?: string; signature?: string; redactedContent?: string } | { type: "context_usage"; contextUsagePercentage: number } | { type: "tool"; name?: string; toolUseId?: string; input?: string; stop?: boolean } | { type: "truncation"; data: string } @@ -138,18 +138,26 @@ export function parseKiroEvent(eventType: string, payload: Uint8Array): ParsedKi : {}), }; case "reasoningContentEvent": - // `text` is plaintext reasoning; `redactedContent` is the encrypted blob the GPT-5.6 family - // (sol/terra/luna) actually returns — they never send `text`. Keyed off the wire field, not - // the model id. Both may be absent on a bare event. - return { - type: "reasoning", - ...(optionalString(eventType, parsed, "text") !== undefined - ? { data: optionalString(eventType, parsed, "text") } - : {}), - ...(optionalString(eventType, parsed, "redactedContent") !== undefined - ? { redactedContent: optionalString(eventType, parsed, "redactedContent") } - : {}), - }; + // `text` is plaintext reasoning; the GPT-5.6 family (sol/terra/luna) instead returns an + // encrypted blob, and the field it arrives on has to be replayed unchanged (see + // kiro/reasoning.ts): `signature` carries the `.KTR~~…` value verbatim and is what every + // capture of those models sent, while `redactedContent` — the base64 shape a capture has + // never shown — stays accepted for any model that sends it. Keyed off the wire field, not the + // model id. Any of the three may be absent on a bare event. + { + const text = optionalString(eventType, parsed, "text"); + const signature = optionalString(eventType, parsed, "signature"); + const redacted = optionalString(eventType, parsed, "redactedContent"); + return { + type: "reasoning", + ...(text !== undefined ? { data: text } : {}), + ...(signature !== undefined + ? { signature } + : redacted !== undefined + ? { redactedContent: redacted } + : {}), + }; + } case "toolUseEvent": return { type: "tool", diff --git a/src/adapters/kiro/payload.ts b/src/adapters/kiro/payload.ts index 4da79a9bcc0..338f525d2eb 100644 --- a/src/adapters/kiro/payload.ts +++ b/src/adapters/kiro/payload.ts @@ -38,7 +38,12 @@ import { validateKiroConversationState, type KiroTurn, } from "./conversation"; -import { injectKiroThinkingTags, kiroNativeEffortField, KIRO_NATIVE_EFFORTS } from "./reasoning"; +import { + injectKiroThinkingTags, + kiroNativeEffortField, + kiroReasoningContent, + KIRO_NATIVE_EFFORTS, +} from "./reasoning"; import { kiroPayloadMessages, userContentText } from "./usage"; import { kiroToolWireNames, @@ -388,7 +393,11 @@ export function buildKiroPayload( assistantResponseMessage: { content: turn.content, ...(turn.toolUses.length > 0 ? { toolUses: turn.toolUses } : {}), - ...(turn.redactedReasoning ? { reasoningContent: { redactedContent: turn.redactedReasoning } } : {}), + // Replayed on the field it was received on: the GPT-5.6 signature is not base64 and is + // rejected when sent as `redactedContent`. + ...(turn.redactedReasoning + ? { reasoningContent: kiroReasoningContent(turn.redactedReasoning) } + : {}), }, } : { diff --git a/src/adapters/kiro/reasoning.ts b/src/adapters/kiro/reasoning.ts index c218bf12339..0441986f10f 100644 --- a/src/adapters/kiro/reasoning.ts +++ b/src/adapters/kiro/reasoning.ts @@ -4,10 +4,25 @@ import type { OcxParsedRequest } from "../../types"; export type KiroReasoningMode = "native" | "emulated"; // Kiro takes a verified native effort field for these models, and each model family names it -// differently: the Sol-only `reasoning.effort` versus the Claude-specific `output_config.effort`. -// Models absent from this table fall back to emulated thinking instructions. +// differently: the GPT-5.6 family's `reasoning.effort` versus the Claude-specific +// `output_config.effort`. Models absent from this table fall back to emulated thinking +// instructions. +// +// The GPT-5.6 entries are measured against the live runtime rather than inferred from the vendor +// schema: the field is accepted (HTTP 200) and the encrypted reasoning blob that comes back grows +// with the effort. On one fixed hard prompt — a primality search plus a 20-bit recurrence count — +// luna's blob measured 5,130 chars at `low`, 16,686 at `medium`, 30,670 at `high` and 48,594 at +// `max`, against 13,118 with no effort signal at all; terra's measured 34,590 and 38,106 at native +// `max` against 11,758 and 17,598 bare, two repetitions each. The channel this replaces — the +// emulated `` tag block, which was all those models used to receive — measured +// 21,202 (`low`) and 28,302 (`max`) for luna, i.e. between that model's native `medium` and +// `high`, never reaching native `max`. `gpt-5.6-sol`'s native `max` cross-checked at 30,498 on the +// same prompt. Terra's absence from this table was therefore an omission rather than a capability +// difference: what the earlier Sol-only scope recorded was not reproducible here. export const KIRO_NATIVE_EFFORT_FIELDS: Record = { "gpt-5.6-sol": "reasoning", + "gpt-5.6-terra": "reasoning", + "gpt-5.6-luna": "reasoning", "claude-opus-5": "output_config", }; @@ -54,3 +69,41 @@ export function injectKiroThinkingTags(content: string, parsed: OcxParsedRequest content, ].join("\n"); } + +/** + * The blob from a Kiro `reasoningContentEvent` has two possible homes on a replayed assistant + * turn, and the wire validates the SHAPE of each rather than its content: `signature` takes the + * emitted string verbatim, while `redactedContent` is a base64 member. The `.KTR~~…` value every + * GPT-5.6 capture returns is NOT valid base64, which is exactly why replaying it as + * `redactedContent` — what this proxy did before the field was measured — came back as + * REQUEST_BODY_INVALID ("Improperly formed request"). + * + * The blob travels as ONE opaque string: adapter event, `ocxr1:` reasoning envelope, then + * `OcxAssistantMessage.kiroRedactedReasoning`. The field it arrived on therefore rides that same + * string, instead of a second parallel value that could drift from it. Provider data cannot forge + * the tag: the other channel is base64, whose alphabet has no colon. + */ +export const KIRO_REASONING_SIGNATURE_TAG = "signature:"; + +export function tagKiroReasoningBlob(field: "signature" | "redactedContent", data: string): string { + return field === "signature" ? KIRO_REASONING_SIGNATURE_TAG + data : data; +} + +/** The wire field a stored blob arrived on, and its untagged value. */ +export function splitKiroReasoningBlob(value: string): { field: "signature" | "redactedContent"; data: string } { + return value.startsWith(KIRO_REASONING_SIGNATURE_TAG) + ? { field: "signature", data: value.slice(KIRO_REASONING_SIGNATURE_TAG.length) } + : { field: "redactedContent", data: value }; +} + +/** + * The `reasoningContent` object on an `assistantResponseMessage`. Exactly one member is set: the + * wire validates the shape, so the two cannot be substituted for each other. + */ +export type KiroReasoningContent = { signature: string } | { redactedContent: string }; + +/** `reasoningContent` for a replayed `assistantResponseMessage`, carrying the blob verbatim. */ +export function kiroReasoningContent(value: string): KiroReasoningContent { + const { field, data } = splitKiroReasoningBlob(value); + return field === "signature" ? { signature: data } : { redactedContent: data }; +} diff --git a/src/adapters/kiro/stream.ts b/src/adapters/kiro/stream.ts index 1740cf8d64e..d10ab1105c0 100644 --- a/src/adapters/kiro/stream.ts +++ b/src/adapters/kiro/stream.ts @@ -19,6 +19,7 @@ import { noteKiroTransientThrottle } from "../kiro-retry"; import { KiroThinkingParser } from "../kiro-thinking"; import { isCompleteKiroToolInput, kiroTruncationErrorMessage } from "../kiro-truncation"; import { isValidKiroConversationId } from "../kiro-wire"; +import { tagKiroReasoningBlob } from "./reasoning"; import { estimateKiroTokens, kiroUpstreamContextWindow } from "./usage"; // Stream parsing (shared by parseStream + parseResponse) @@ -633,8 +634,13 @@ async function* parseKiroAttemptEvents( if (ev.data) { yield* emitRetained(stage({ type: "reasoning_raw_delta", text: ev.data })); } - if (ev.redactedContent) { - yield* emitRetained(stage({ type: "kiro_redacted_reasoning", data: ev.redactedContent })); + // The blob is replayed on the field it arrived on, so remember that field here — this is + // the only place that still knows it. See kiro/reasoning.ts for why the distinction is + // load-bearing rather than cosmetic. + if (ev.signature) { + yield* emitRetained(stage({ type: "kiro_redacted_reasoning", data: tagKiroReasoningBlob("signature", ev.signature) })); + } else if (ev.redactedContent) { + yield* emitRetained(stage({ type: "kiro_redacted_reasoning", data: tagKiroReasoningBlob("redactedContent", ev.redactedContent) })); } break; case "context_usage": diff --git a/src/adapters/kiro/wire.ts b/src/adapters/kiro/wire.ts index ec8c32272da..7bf91d9db55 100644 --- a/src/adapters/kiro/wire.ts +++ b/src/adapters/kiro/wire.ts @@ -1,5 +1,6 @@ import type { OcxProviderConfig } from "../../types"; import type { KiroImage } from "../kiro-images"; +import type { KiroReasoningContent } from "./reasoning"; export const AMZ_TARGET = "AmazonCodeWhispererStreamingService.GenerateAssistantResponse"; export const SDK_VERSION = "1.0.27"; @@ -51,7 +52,7 @@ export interface KiroHistoryEntry { assistantResponseMessage?: { content: string; toolUses?: KiroToolUse[]; - reasoningContent?: { redactedContent: string }; + reasoningContent?: KiroReasoningContent; }; } diff --git a/src/providers/kiro-models.ts b/src/providers/kiro-models.ts index 72063fde9b5..2f8e0159769 100644 --- a/src/providers/kiro-models.ts +++ b/src/providers/kiro-models.ts @@ -47,9 +47,10 @@ export const KIRO_MODEL_CONTEXT_WINDOWS: Record = { const KIRO_REASONING_EFFORTS = ["low", "medium", "high", "xhigh", "max"]; -// gpt-5.6-sol and claude-opus-5 send these values through Kiro's verified native effort fields -// (`reasoning.effort` and `output_config.effort` respectively). Other models map them to bounded -// thinking instructions until their native effort support is verified. +// The GPT-5.6 family (sol/terra/luna) and claude-opus-5 send these values through Kiro's verified +// native effort fields (`reasoning.effort` for the GPT-5.6 models, `output_config.effort` for +// claude-opus-5). Other models map them to bounded thinking instructions until their native +// effort support is verified. export const KIRO_MODEL_REASONING_EFFORTS: Record = Object.fromEntries( KIRO_MODELS.map(id => [id, KIRO_REASONING_EFFORTS]), ); diff --git a/src/responses/reasoning-envelope.ts b/src/responses/reasoning-envelope.ts index ba20e800edf..9b97dd060ab 100644 --- a/src/responses/reasoning-envelope.ts +++ b/src/responses/reasoning-envelope.ts @@ -28,9 +28,12 @@ export interface ReasoningEnvelope { */ txt?: string; /** - * Kiro `reasoningContentEvent.redactedContent`: a KMS-encrypted reasoning blob that is opaque to - * the proxy. Kiro's own CLI replays it on the matching `assistantResponseMessage` to preserve - * model reasoning across turns, so it round-trips here the same way a signature does. + * Kiro's reasoning blob from `reasoningContentEvent`: a KMS-encrypted value that is opaque to the + * proxy (the GPT-5.6 family sends it as `signature`, other models as the base64 + * `redactedContent`, and the value carries a tag naming which one — see + * src/adapters/kiro/reasoning.ts). Kiro's own CLI replays it on the matching + * `assistantResponseMessage` to preserve model reasoning across turns, so it round-trips here the + * same way a signature does. */ krc?: string; } diff --git a/src/types/request.ts b/src/types/request.ts index cec8294a502..3ac5e2cbde4 100644 --- a/src/types/request.ts +++ b/src/types/request.ts @@ -154,9 +154,11 @@ export interface OcxAssistantMessage { model?: string; timestamp: number; /** - * Kiro `reasoningContent.redactedContent` for THIS assistant turn — an opaque encrypted blob - * Kiro replays to preserve model reasoning across turns. Provider-specific and unrenderable, so - * it rides the message rather than a content part: any other adapter simply ignores it. + * Kiro's encrypted reasoning blob for THIS assistant turn — the opaque value from the turn's + * `reasoningContentEvent` (`signature` for the GPT-5.6 family, `redactedContent` for the base64 + * shape), tagged with the wire field it must be replayed on (see kiro/reasoning.ts). Kiro + * replays it to preserve model reasoning across turns. Provider-specific and unrenderable, so it + * rides the message rather than a content part: any other adapter simply ignores it. */ kiroRedactedReasoning?: string; } @@ -317,8 +319,9 @@ export type AdapterEvent = // opaque redacted_thinking blocks. Both must be replayed verbatim or tool-use turns 400. | { type: "thinking_signature"; signature: string } | { type: "redacted_thinking"; data: string } - // Kiro reasoning round-trip: the encrypted `redactedContent` blob for the CURRENT assistant turn. - // Never rendered — it only rides the reasoning item's envelope so the next request can replay it. + // Kiro reasoning round-trip: the encrypted reasoning blob for the CURRENT assistant turn, tagged + // with the wire field it arrived on. Never rendered — it only rides the reasoning item's envelope + // so the next request can replay it verbatim. | { type: "kiro_redacted_reasoning"; data: string } | { type: "reasoning_raw_delta"; text: string } | { type: "tool_call_start"; id: string; name: string; providerMetadata?: OcxProviderOpaqueToolCallMetadata } diff --git a/structure/providers/kiro.md b/structure/providers/kiro.md index 80f765adae8..39494ed80ee 100644 --- a/structure/providers/kiro.md +++ b/structure/providers/kiro.md @@ -31,26 +31,38 @@ raw body. > Decision record: [ADR-0061](../decisions/ADR-0061-kiro-responses-text-controls.md) -## Kiro reasoning round-trip (`redactedContent`) +## Kiro reasoning round-trip (`signature`) Kiro never returns plaintext reasoning for its **GPT-5.6 family** (`gpt-5.6-sol`, `-terra`, -`-luna`): `reasoningContentEvent` carries a KMS-encrypted `redactedContent` blob, never `text`. -Their `additionalModelRequestFieldsSchema` (`ListAvailableModels`) accepts only `reasoning.effort` -with `additionalProperties: false` — there is no display/summary opt-in, so this is the only -reasoning these models can return. Kiro's own CLI replays the blob on the matching -`assistantResponseMessage.reasoningContent` to preserve model reasoning across 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. +`-luna`): `reasoningContentEvent` carries a KMS-encrypted blob, never `text`. It arrives on +`signature`, holding the `.KTR~~…` value verbatim, which is what every capture of those models +sent. Their `additionalModelRequestFieldsSchema` (`ListAvailableModels`) accepts only +`reasoning.effort` with `additionalProperties: false` — there is no display/summary opt-in, so this +is the only reasoning these models can return, and all three select that native field +(`KIRO_NATIVE_EFFORT_FIELDS` in `src/adapters/kiro/reasoning.ts`). Kiro's own CLI replays the blob +on the matching `assistantResponseMessage.reasoningContent` to preserve model reasoning across +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. + +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 +`~` — so a blob replayed as `redactedContent` is rejected with `REQUEST_BODY_INVALID` +("Improperly formed request"). `signature` therefore takes the verbatim value and +`redactedContent` remains the home for the base64 shape another model may send. Which field a blob +arrived on is carried by the blob itself, one opaque string with a `signature:` tag, rather than by +a second value that could drift from it; provider data cannot forge the tag, because base64 has no +colon. The Claude 4.6+/5 entries advertise a different, richer contract (`thinking.type` adaptive/disabled, `thinking.display` summarized/omitted, `output_config.effort`, `max_tokens`) and are not covered by that measurement; older Claude, deepseek, minimax, glm, and qwen entries advertise no additional fields at all. The handling below keys off the wire field, not the model id, so any model that -sends `redactedContent` round-trips. +sends either member round-trips. -- The blob rides the existing `ocxr1:` envelope as `krc` (`src/responses/reasoning-envelope.ts`) on - an envelope-only reasoning item — `summary: []`, no text deltas — so it stays invisible in the - Codex app while round-tripping, exactly like the hidden-thinking path. +- The tagged blob rides the existing `ocxr1:` envelope as `krc` + (`src/responses/reasoning-envelope.ts`) on an envelope-only reasoning item — `summary: []`, no + text deltas — so it stays invisible in the Codex app while round-tripping, exactly like the + hidden-thinking path. - **Pairing is backwards.** Kiro emits `reasoningContentEvent` at the END of an assistant turn, after content AND tool calls. A `krc`-only item therefore belongs to the turn that already closed, so the parser attaches it to the PRECEDING assistant message rather than folding it into diff --git a/tests/providers/kiro/kiro-adapter.test.ts b/tests/providers/kiro/kiro-adapter.test.ts index 947d6ad7401..d068f905b27 100644 --- a/tests/providers/kiro/kiro-adapter.test.ts +++ b/tests/providers/kiro/kiro-adapter.test.ts @@ -1741,7 +1741,7 @@ describe("kiro adapter — native and emulated reasoning effort", () => { }); test("native-effort models reject efforts Kiro does not accept", async () => { - for (const modelId of ["gpt-5.6-sol", "claude-opus-5"]) { + for (const modelId of ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "claude-opus-5"]) { await expect(createKiroAdapter(provider).buildRequest({ ...parsedWith([{ role: "user", content: "solve" }], undefined, modelId), options: { reasoning: "minimal" }, diff --git a/tests/providers/kiro/kiro-reasoning-roundtrip.test.ts b/tests/providers/kiro/kiro-reasoning-roundtrip.test.ts index 9cf598f3725..8c1dc5cd373 100644 --- a/tests/providers/kiro/kiro-reasoning-roundtrip.test.ts +++ b/tests/providers/kiro/kiro-reasoning-roundtrip.test.ts @@ -1,9 +1,14 @@ import { describe, expect, test } from "bun:test"; +import { buildKiroPayload } from "../../../src/adapters/kiro/payload"; import { bridgeToResponsesSSE, buildResponseJSON } from "../../../src/bridge"; import { parseRequest } from "../../../src/responses/parser"; import { decodeReasoningEnvelope } from "../../../src/responses/reasoning-envelope"; import type { AdapterEvent } from "../../../src/types"; +import type { OcxProviderConfig } from "../../../src/types"; +import { createKiroAdapter as createKiroAdapterProduction } from "../../../src/adapters/kiro"; +import { encodeMessage } from "../../../src/lib/eventstream-decoder"; import { createTranslatorBudget } from "../../../src/lib/translator-budget"; +import { withTestTranslatorBudget } from "../../helpers/translator-budget"; const BLOB = "LktUUn5+ZXlKbGJtTnllWEIwYVc5dVVtVm5hVzl1SWpvaQ=="; @@ -135,3 +140,140 @@ describe("kiro redacted-reasoning round-trip (bridge → parse)", () => { expect((assistant as { kiroRedactedReasoning?: string }).kiroRedactedReasoning).toBe(BLOB); }); }); + +// The blob has two possible homes on a replayed `assistantResponseMessage`, and the wire validates +// the SHAPE of each: `signature` takes the emitted string verbatim, while `redactedContent` is a +// base64 member. The ".KTR~~…" value the GPT-5.6 family returns is not base64, which is why +// replaying it as `redactedContent` — what the proxy did before the field was measured — came back +// as REQUEST_BODY_INVALID. Which field a blob arrived on therefore has to survive the whole +// round-trip, not just the parse. +describe("kiro reasoning blob — the wire field it replays on", () => { + const SIGNATURE = ".KTR~~eyJlbmNyeXB0aW9uUmVnaW9uIjoidXMtZWFzdC0xIiwic2xvdHMiOltdfQ=="; + + interface HistoryEntry { + assistantResponseMessage?: { reasoningContent?: unknown }; + } + + /** Round-trip one blob the way Codex does — bridge, history replay, then the next Kiro body. */ + function replayedReasoningContent(blob: string): unknown { + const response = buildResponseJSON([ + { type: "text_delta", text: "the answer" }, + { type: "kiro_redacted_reasoning", data: blob }, + { type: "done", usage: { inputTokens: 1, outputTokens: 2 }, endTurn: true }, + ], "kiro/gpt-5.6-luna"); + const items = (response.output as Record[]).map(({ status: _status, ...item }) => item); + // Kiro requires the request to end with a user turn, so the replayed turn is followed by one. + const parsed = parseRequest({ + model: "kiro/gpt-5.6-luna", + input: [ + { type: "message", role: "user", content: [{ type: "input_text", text: "hi" }] }, + ...items, + { type: "message", role: "user", content: [{ type: "input_text", text: "again" }] }, + ], + }); + const { payload } = buildKiroPayload(parsed, undefined, "disabled", "ide"); + const history = (payload.conversationState as { history?: HistoryEntry[] }).history ?? []; + return history.find(entry => entry.assistantResponseMessage?.reasoningContent) + ?.assistantResponseMessage?.reasoningContent; + } + + test("a signature blob is replayed verbatim on `signature`", () => { + expect(replayedReasoningContent(`signature:${SIGNATURE}`)).toEqual({ signature: SIGNATURE }); + }); + + test("an untagged blob keeps the base64 `redactedContent` shape", () => { + expect(replayedReasoningContent(BLOB)).toEqual({ redactedContent: BLOB }); + }); + + test("the tag never reaches the wire as part of the blob", () => { + const replayed = replayedReasoningContent(`signature:${SIGNATURE}`) as { signature?: string }; + expect(replayed.signature).toBe(SIGNATURE); + expect(JSON.stringify(replayed)).not.toContain("signature:"); + }); +}); + +// The parse side is where the tag is minted, so it is pinned here rather than in +// tests/providers/kiro/kiro-stream.test.ts: that file sits at its file-size-ratchet cap +// (tests/fixtures/file-size-baseline.json), and a baselined file may not grow by one line. +// An event carrying only the signature still has to emit the blob — the GPT-5.6 family can finish +// a turn with the encrypted blob and no assistant text at all. +describe("kiro reasoning blob — the stream records the field it arrived on", () => { + const provider = { + adapter: "kiro", + baseUrl: "https://runtime.us-east-1.kiro.dev", + authMode: "oauth", + apiKey: "tok-123", + } as unknown as OcxProviderConfig; + const enc = new TextEncoder(); + const signatureFrame = (obj: unknown) => encodeMessage( + { ":message-type": "event", ":event-type": "reasoningContentEvent" }, + enc.encode(JSON.stringify(obj)), + ); + + function streamOf(...frames: Uint8Array[]): ReadableStream { + let i = 0; + return new ReadableStream({ + pull(c) { + if (i < frames.length) c.enqueue(frames[i++]); + else c.close(); + }, + }); + } + + async function parse(frame: Uint8Array): Promise { + const adapter = withTestTranslatorBudget(createKiroAdapterProduction(provider)); + const out: AdapterEvent[] = []; + for await (const event of adapter.parseStream(new Response(streamOf(frame)))) out.push(event); + return out; + } + + test("a signature blob is tagged with the field it must be replayed on", async () => { + // Every capture of the GPT-5.6 family put the blob on `signature` and left `text` as a "..." + // placeholder. That value starts with ".KTR~~", which is NOT base64, so replaying it as + // `redactedContent` — what the proxy used to send — is rejected as REQUEST_BODY_INVALID. A + // `redactedContent` event stays untagged; the untagged shape is covered above. + const signature = ".KTR~~eyJ2IjoxfQ=="; + expect(await parse(signatureFrame({ signature, text: "..." }))).toEqual([ + { type: "reasoning_raw_delta", text: "..." }, + { type: "kiro_redacted_reasoning", data: `signature:${signature}` }, + expect.objectContaining({ type: "done" }), + ]); + }); + + test("a signature-only event still yields the tagged blob", async () => { + // No assistant text means no terminal either: the blob is the whole turn, which is why the tag + // must not be conditioned on `text`. + expect((await parse(signatureFrame({ signature: ".KTR~~only" })))[0]).toEqual( + { type: "kiro_redacted_reasoning", data: "signature:.KTR~~only" }, + ); + }); +}); + +// The request side of the same story. luna and terra used to fall through to the emulated +// block, a strictly weaker signal: on one fixed hard prompt that channel landed +// between the model's native medium and high (21,202 / 28,302 chars) and never reached native max +// (48,594), while the native ladder itself ran 5,130 -> 48,594 from low to max. The whole GPT-5.6 +// family shares the field name, so all three are native now. +describe("kiro native reasoning effort — the GPT-5.6 family", () => { + function wireBody(modelId: string): Record { + const parsed = { + modelId, + stream: true, + options: { reasoning: "max", maxOutputTokens: 1000 }, + context: { messages: [{ role: "user", content: "solve" }] }, + } as unknown as Parameters[0]; + return buildKiroPayload(parsed, undefined, "disabled", "ide").payload; + } + + test("luna and terra send the native reasoning field instead of thinking tags", () => { + for (const modelId of ["gpt-5.6-luna", "gpt-5.6-terra"]) { + const body = wireBody(modelId); + expect(body.additionalModelRequestFields).toEqual({ reasoning: { effort: "max" } }); + // Native effort replaces the emulated thinking-tag prompt entirely. + const current = (body.conversationState as { + currentMessage: { userInputMessage: { content: string } }; + }).currentMessage.userInputMessage.content; + expect(current).toBe("solve"); + } + }); +}); From c68682d0c2ed89ed4e79c39b725211ebb8e95215 Mon Sep 17 00:00:00 2001 From: "wentao.ma2" Date: Tue, 15 Sep 2026 15:28:33 +0800 Subject: [PATCH 2/3] docs(kiro): spell the full claude-opus-5 effort field in translated pages CodeRabbit flagged the ja/ko/ru adapter pages for dropping the `additionalModelRequestFields` prefix on the claude-opus-5 effort field, which documents a different request shape than the English source. zh-cn and zh-tw carried the same truncation, so all five locales now name `additionalModelRequestFields.output_config.effort` exactly as the canonical page does. tr and fr were already complete. --- docs-site/src/content/docs/ja/reference/adapters.md | 2 +- docs-site/src/content/docs/ko/reference/adapters.md | 2 +- docs-site/src/content/docs/ru/reference/adapters.md | 2 +- docs-site/src/content/docs/zh-cn/reference/adapters.md | 2 +- docs-site/src/content/docs/zh-tw/reference/adapters.md | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs-site/src/content/docs/ja/reference/adapters.md b/docs-site/src/content/docs/ja/reference/adapters.md index 2191cd41dd0..d9dd6fdd0b1 100644 --- a/docs-site/src/content/docs/ja/reference/adapters.md +++ b/docs-site/src/content/docs/ja/reference/adapters.md @@ -156,7 +156,7 @@ filtered incomplete になります。実際のツール呼び出しを伴わな `gpt-5.6-sol` / `gpt-5.6-terra` / `gpt-5.6-luna` と `claude-opus-5` はネイティブ effort をサポートし、リクエストフィールド名が異なります。 `low` / `medium` / `high` / `xhigh` / `max` は、GPT-5.6 系では -`additionalModelRequestFields.reasoning.effort`、`claude-opus-5` では `output_config.effort` として送信されます。 +`additionalModelRequestFields.reasoning.effort`、`claude-opus-5` では `additionalModelRequestFields.output_config.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 4ff2cc0b370..548977d38cb 100644 --- a/docs-site/src/content/docs/ko/reference/adapters.md +++ b/docs-site/src/content/docs/ko/reference/adapters.md @@ -169,7 +169,7 @@ commentary로 유지하고 비공개 완료 툴을 한 번 검증합니다. `gpt-5.6-sol`, `gpt-5.6-terra`, `gpt-5.6-luna`와 `claude-opus-5`는 네이티브 effort를 지원하며 요청 필드 이름이 다릅니다. `low` / `medium` / `high` / `xhigh` / `max` 값은 GPT-5.6 계열에서는 -`additionalModelRequestFields.reasoning.effort`, `claude-opus-5`에서는 `output_config.effort`로 전송됩니다. +`additionalModelRequestFields.reasoning.effort`, `claude-opus-5`에서는 `additionalModelRequestFields.output_config.effort`로 전송됩니다. ## `cursor` diff --git a/docs-site/src/content/docs/ru/reference/adapters.md b/docs-site/src/content/docs/ru/reference/adapters.md index 9961210917f..62416d5671d 100644 --- a/docs-site/src/content/docs/ru/reference/adapters.md +++ b/docs-site/src/content/docs/ru/reference/adapters.md @@ -191,7 +191,7 @@ incomplete. `TOOL_USE` без фактического вызова инстру Модели семейства GPT-5.6 (`gpt-5.6-sol`, `gpt-5.6-terra`, `gpt-5.6-luna`) и `claude-opus-5` поддерживают нативный effort, но называют поле запроса по-разному. Значения `low` / `medium` / `high` / `xhigh` / `max` отправляются как -`additionalModelRequestFields.reasoning.effort` для моделей GPT-5.6 и `output_config.effort` для `claude-opus-5`. +`additionalModelRequestFields.reasoning.effort` для моделей GPT-5.6 и `additionalModelRequestFields.output_config.effort` для `claude-opus-5`. ## `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 6c68bf35265..e7f96a93e4f 100644 --- a/docs-site/src/content/docs/zh-cn/reference/adapters.md +++ b/docs-site/src/content/docs/zh-cn/reference/adapters.md @@ -156,7 +156,7 @@ Kiro 的 assistant 文本本身没有可靠的回合结束标记,但终止的 `gpt-5.6-sol`、`gpt-5.6-terra`、`gpt-5.6-luna` 和 `claude-opus-5` 支持原生 effort,且请求字段名不同。`low` / `medium` / `high` / `xhigh` / `max` 在 GPT-5.6 系列中通过 `additionalModelRequestFields.reasoning.effort` 发送, -在 `claude-opus-5` 上通过 `output_config.effort` 发送。 +在 `claude-opus-5` 上通过 `additionalModelRequestFields.output_config.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 90cb9f8a3fb..4708e2e6b18 100644 --- a/docs-site/src/content/docs/zh-tw/reference/adapters.md +++ b/docs-site/src/content/docs/zh-tw/reference/adapters.md @@ -147,7 +147,7 @@ Kiro 的 assistant 文字本身沒有可靠的回合結束標記,但終止的 `gpt-5.6-sol`、`gpt-5.6-terra`、`gpt-5.6-luna` 和 `claude-opus-5` 支援原生 effort,且請求欄位名不同。`low` / `medium` / `high` / `xhigh` / `max` 在 GPT-5.6 系列中透過 `additionalModelRequestFields.reasoning.effort` 傳送, -在 `claude-opus-5` 上透過 `output_config.effort` 傳送。 +在 `claude-opus-5` 上透過 `additionalModelRequestFields.output_config.effort` 傳送。 ## `cursor` From 46e5f55ca70dc8cab0433a05f07229d48435e2a8 Mon Sep 17 00:00:00 2001 From: JUN Date: Tue, 15 Sep 2026 19:51:32 +0900 Subject: [PATCH 3/3] fix(kiro): keep unverified luna and terra effort rungs emulated Use the proven native effort allowlist for newly enabled models, retain existing Sol/Opus behavior, and add boundary fixtures. No live provider requests or product tests were run on the connected machine. Co-authored-by: wentao.ma2 --- .../src/content/docs/fr/reference/adapters.md | 7 +++- .../src/content/docs/ja/reference/adapters.md | 10 +++-- .../src/content/docs/ko/reference/adapters.md | 10 +++-- .../src/content/docs/reference/adapters.md | 15 ++++---- .../src/content/docs/ru/reference/adapters.md | 10 +++-- .../src/content/docs/tr/reference/adapters.md | 15 +++----- .../content/docs/zh-cn/reference/adapters.md | 10 +++-- .../content/docs/zh-tw/reference/adapters.md | 10 +++-- src/adapters/kiro/payload.ts | 7 +++- src/adapters/kiro/reasoning.ts | 20 +++++++--- structure/providers/kiro.md | 5 +++ .../kiro/kiro-reasoning-roundtrip.test.ts | 37 +++++++++++++++---- 12 files changed, 105 insertions(+), 51 deletions(-) diff --git a/docs-site/src/content/docs/fr/reference/adapters.md b/docs-site/src/content/docs/fr/reference/adapters.md index dc28832ca7e..9535fe02f44 100644 --- a/docs-site/src/content/docs/fr/reference/adapters.md +++ b/docs-site/src/content/docs/fr/reference/adapters.md @@ -131,7 +131,12 @@ Si Kiro s’arrête sans appeler l’outil d’achèvement, l’adaptateur effec ### Effort de raisonnement -La famille GPT-5.6 de Kiro (`gpt-5.6-sol`, `gpt-5.6-terra`, `gpt-5.6-luna`) et `claude-opus-5` prennent en charge nativement un niveau d’effort vérifié, mais chaque famille de modèles nomme différemment le champ de la requête. La valeur sélectionnée `low`, `medium`, `high`, `xhigh` ou `max` est envoyée dans `additionalModelRequestFields.reasoning.effort` pour les modèles GPT-5.6, et dans `additionalModelRequestFields.output_config.effort` pour `claude-opus-5`. Les autres modèles Kiro utilisent actuellement un raisonnement émulé : opencodex convertit le niveau choisi en instructions de réflexion bornées dans le contenu utilisateur, car leur champ d’effort natif n’a pas été vérifié. La présence d’un contrôle d’effort annoncé sur ces modèles ne prouve donc pas la prise en charge native du raisonnement en amont. +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. ## `cursor` diff --git a/docs-site/src/content/docs/ja/reference/adapters.md b/docs-site/src/content/docs/ja/reference/adapters.md index d9dd6fdd0b1..1565770c954 100644 --- a/docs-site/src/content/docs/ja/reference/adapters.md +++ b/docs-site/src/content/docs/ja/reference/adapters.md @@ -154,10 +154,12 @@ filtered incomplete になります。実際のツール呼び出しを伴わな ### Reasoning effort -`gpt-5.6-sol` / `gpt-5.6-terra` / `gpt-5.6-luna` と `claude-opus-5` はネイティブ effort をサポートし、リクエストフィールド名が異なります。 -`low` / `medium` / `high` / `xhigh` / `max` は、GPT-5.6 系では -`additionalModelRequestFields.reasoning.effort`、`claude-opus-5` では `additionalModelRequestFields.output_config.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 の選択肢だけではネイティブ対応を意味しません。 ## `cursor` diff --git a/docs-site/src/content/docs/ko/reference/adapters.md b/docs-site/src/content/docs/ko/reference/adapters.md index 548977d38cb..64193b42328 100644 --- a/docs-site/src/content/docs/ko/reference/adapters.md +++ b/docs-site/src/content/docs/ko/reference/adapters.md @@ -167,10 +167,12 @@ commentary로 유지하고 비공개 완료 툴을 한 번 검증합니다. ### Reasoning effort -`gpt-5.6-sol`, `gpt-5.6-terra`, `gpt-5.6-luna`와 `claude-opus-5`는 네이티브 effort를 지원하며 요청 필드 이름이 다릅니다. -`low` / `medium` / `high` / `xhigh` / `max` 값은 GPT-5.6 계열에서는 -`additionalModelRequestFields.reasoning.effort`, `claude-opus-5`에서는 `additionalModelRequestFields.output_config.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는 에뮬레이션이며, 조절 항목이 있다고 네이티브 지원을 뜻하지는 않습니다. ## `cursor` diff --git a/docs-site/src/content/docs/reference/adapters.md b/docs-site/src/content/docs/reference/adapters.md index f5e23106240..70180417f19 100644 --- a/docs-site/src/content/docs/reference/adapters.md +++ b/docs-site/src/content/docs/reference/adapters.md @@ -364,14 +364,13 @@ important than cosmetic de-duplication. Tool-free requests retain normal text co ### Reasoning effort -The Kiro GPT-5.6 family (`gpt-5.6-sol`, `gpt-5.6-terra`, `gpt-5.6-luna`) and `claude-opus-5` have -verified native effort support, and each model family names the request field differently. A -selected `low`, `medium`, `high`, `xhigh`, or `max` value is sent as -`additionalModelRequestFields.reasoning.effort` for the GPT-5.6 models and as -`additionalModelRequestFields.output_config.effort` for `claude-opus-5`. Other Kiro models currently -use emulated reasoning: opencodex converts the selected level into bounded thinking instructions in -the user content because their native effort field has not been verified. Do not interpret an -advertised effort control on those models as proof of upstream-native reasoning support. +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. ## `cursor` diff --git a/docs-site/src/content/docs/ru/reference/adapters.md b/docs-site/src/content/docs/ru/reference/adapters.md index 62416d5671d..1d81b039e23 100644 --- a/docs-site/src/content/docs/ru/reference/adapters.md +++ b/docs-site/src/content/docs/ru/reference/adapters.md @@ -189,10 +189,12 @@ incomplete. `TOOL_USE` без фактического вызова инстру ### Reasoning effort -Модели семейства GPT-5.6 (`gpt-5.6-sol`, `gpt-5.6-terra`, `gpt-5.6-luna`) и `claude-opus-5` поддерживают нативный effort, но называют поле запроса по-разному. -Значения `low` / `medium` / `high` / `xhigh` / `max` отправляются как -`additionalModelRequestFields.reasoning.effort` для моделей GPT-5.6 и `additionalModelRequestFields.output_config.effort` для `claude-opus-5`. - +Семейство 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 не доказывает нативную поддержку. ## `cursor` diff --git a/docs-site/src/content/docs/tr/reference/adapters.md b/docs-site/src/content/docs/tr/reference/adapters.md index 876d050b046..090be990616 100644 --- a/docs-site/src/content/docs/tr/reference/adapters.md +++ b/docs-site/src/content/docs/tr/reference/adapters.md @@ -268,15 +268,12 @@ tam olarak tekrarlasa bile, çünkü aşama doğruluğu kozmetik tekilleştirmed ### Akıl yürütme çabası -Kiro'nun GPT-5.6 ailesi (`gpt-5.6-sol`, `gpt-5.6-terra`, `gpt-5.6-luna`) ve `claude-opus-5` doğrulanmış yerel çaba desteğine sahiptir ve -her model ailesi istek alanını farklı şekilde adlandırır. Seçilen `low`, -`medium`, `high`, `xhigh` veya `max` değeri GPT-5.6 modelleri için -`additionalModelRequestFields.reasoning.effort` olarak ve `claude-opus-5` için -`additionalModelRequestFields.output_config.effort` olarak gönderilir. Diğer -Kiro modelleri şu anda öykünülmüş akıl yürütme kullanır: opencodex yerel çaba -alanları doğrulanmadığı için seçilen seviyeyi kullanıcı içeriğinde sınırlı -düşünme talimatlarına dönüştürür. Bu modellerde bildirilen bir çaba denetimini -yukarı akış yerel akıl yürütme desteğinin kanıtı olarak yorumlamayın. +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. ## `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 e7f96a93e4f..a970fd1d6c9 100644 --- a/docs-site/src/content/docs/zh-cn/reference/adapters.md +++ b/docs-site/src/content/docs/zh-cn/reference/adapters.md @@ -154,10 +154,12 @@ Kiro 的 assistant 文本本身没有可靠的回合结束标记,但终止的 ### Reasoning effort -`gpt-5.6-sol`、`gpt-5.6-terra`、`gpt-5.6-luna` 和 `claude-opus-5` 支持原生 effort,且请求字段名不同。`low` / `medium` / `high` / -`xhigh` / `max` 在 GPT-5.6 系列中通过 `additionalModelRequestFields.reasoning.effort` 发送, -在 `claude-opus-5` 上通过 `additionalModelRequestFields.output_config.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 选项并不代表原生支持。 ## `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 4708e2e6b18..2b314cd2992 100644 --- a/docs-site/src/content/docs/zh-tw/reference/adapters.md +++ b/docs-site/src/content/docs/zh-tw/reference/adapters.md @@ -145,10 +145,12 @@ Kiro 的 assistant 文字本身沒有可靠的回合結束標記,但終止的 ### Reasoning effort -`gpt-5.6-sol`、`gpt-5.6-terra`、`gpt-5.6-luna` 和 `claude-opus-5` 支援原生 effort,且請求欄位名不同。`low` / `medium` / `high` / -`xhigh` / `max` 在 GPT-5.6 系列中透過 `additionalModelRequestFields.reasoning.effort` 傳送, -在 `claude-opus-5` 上透過 `additionalModelRequestFields.output_config.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 選項不代表原生支援。 ## `cursor` diff --git a/src/adapters/kiro/payload.ts b/src/adapters/kiro/payload.ts index 338f525d2eb..1fbd3bcbfd0 100644 --- a/src/adapters/kiro/payload.ts +++ b/src/adapters/kiro/payload.ts @@ -456,7 +456,12 @@ export function buildKiroPayload( if (!KIRO_NATIVE_EFFORTS.includes(effort)) { throw new Error(`Kiro ${normalizeKiroModelId(parsed.modelId)} does not support reasoning effort ${JSON.stringify(effort)}`); } - payload.additionalModelRequestFields = { [effortField]: { effort } }; + // Model eligibility still owns unsupported-effort validation above; wire eligibility + // is narrower for luna/terra, whose unverified rungs retain the thinking-tag path. + const verifiedEffortField = kiroNativeEffortField(parsed.modelId, effort); + if (verifiedEffortField) { + payload.additionalModelRequestFields = { [verifiedEffortField]: { effort } }; + } } if (profileArn) payload.profileArn = profileArn; return { payload, nameMap, conversationId, completionMode }; diff --git a/src/adapters/kiro/reasoning.ts b/src/adapters/kiro/reasoning.ts index 0441986f10f..d12c95654ca 100644 --- a/src/adapters/kiro/reasoning.ts +++ b/src/adapters/kiro/reasoning.ts @@ -28,12 +28,22 @@ export const KIRO_NATIVE_EFFORT_FIELDS: Record block, a strictly weaker signal: on one fixed hard prompt that channel landed // between the model's native medium and high (21,202 / 28,302 chars) and never reached native max // (48,594), while the native ladder itself ran 5,130 -> 48,594 from low to max. The whole GPT-5.6 -// family shares the field name, so all three are native now. +// family shares the field name, but luna/terra keep xhigh emulated until verified. describe("kiro native reasoning effort — the GPT-5.6 family", () => { - function wireBody(modelId: string): Record { + function wireBody(modelId: string, effort = "max"): Record { const parsed = { modelId, stream: true, - options: { reasoning: "max", maxOutputTokens: 1000 }, + options: { reasoning: effort, maxOutputTokens: 1000 }, context: { messages: [{ role: "user", content: "solve" }] }, } as unknown as Parameters[0]; return buildKiroPayload(parsed, undefined, "disabled", "ide").payload; @@ -267,13 +268,35 @@ describe("kiro native reasoning effort — the GPT-5.6 family", () => { test("luna and terra send the native reasoning field instead of thinking tags", () => { for (const modelId of ["gpt-5.6-luna", "gpt-5.6-terra"]) { - const body = wireBody(modelId); - expect(body.additionalModelRequestFields).toEqual({ reasoning: { effort: "max" } }); - // Native effort replaces the emulated thinking-tag prompt entirely. + for (const effort of ["low", "medium", "high", "max"]) { + const body = wireBody(modelId, effort); + expect(body.additionalModelRequestFields).toEqual({ reasoning: { effort } }); + // Native effort replaces the emulated thinking-tag prompt entirely. + const current = (body.conversationState as { + currentMessage: { userInputMessage: { content: string } }; + }).currentMessage.userInputMessage.content; + expect(current).toBe("solve"); + } + } + }); + + test("luna and terra keep unverified xhigh on the emulated path", () => { + for (const modelId of ["gpt-5.6-luna", "gpt-5.6-terra"]) { + const body = wireBody(modelId, "xhigh"); + expect(body.additionalModelRequestFields).toBeUndefined(); const current = (body.conversationState as { currentMessage: { userInputMessage: { content: string } }; }).currentMessage.userInputMessage.content; - expect(current).toBe("solve"); + expect(current).toContain("enabled"); + expect(current).toContain("900"); + expect(kiroNativeEffortField(modelId, "future-effort")).toBeUndefined(); } }); + + test("existing Sol and Opus native xhigh fields stay unchanged", () => { + expect(wireBody("gpt-5.6-sol", "xhigh").additionalModelRequestFields) + .toEqual({ reasoning: { effort: "xhigh" } }); + expect(wireBody("claude-opus-5", "xhigh").additionalModelRequestFields) + .toEqual({ output_config: { effort: "xhigh" } }); + }); });