From 584b46a37369c83928e9e3fed7fc9ffeb1d9606e Mon Sep 17 00:00:00 2001 From: svc-aibot-363 <271381517+svc-aibot-363@users.noreply.github.com> Date: Mon, 9 Nov 2026 12:54:04 +0100 Subject: [PATCH 01/17] fix(kiro): send the full tool contract and native effort to Kiro Claude models Refs #8306 --- src/adapters/kiro-tools.ts | 42 ++-- src/adapters/kiro/payload.ts | 5 +- src/adapters/kiro/reasoning.ts | 13 +- src/providers/kiro-models.ts | 8 +- src/server/responses/request-prepare.ts | 1 + src/types/request.ts | 2 + tests/providers/kiro/kiro-adapter.test.ts | 184 ++++++++++++++++-- .../kiro/kiro-reasoning-roundtrip.test.ts | 49 +++++ .../server-kiro-structured-output-e2e.test.ts | 41 ++++ 9 files changed, 311 insertions(+), 34 deletions(-) diff --git a/src/adapters/kiro-tools.ts b/src/adapters/kiro-tools.ts index 4466ddf7133..f643f645921 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. +// Claude models accept every one of these keys (live probe on 2026-11-07, one key per request +// plus a full Claude Code catalog), so their schemas keep the constraints the model needs. const KIRO_REJECTED_SCHEMA_KEYS = new Set([ "additionalProperties", "pattern", @@ -65,28 +71,30 @@ 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(); + +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 +117,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 +132,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 +155,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 +205,7 @@ export function convertKiroToolContext( ): { tools: unknown[]; systemAdditions: string[]; nameMap: Map; registry: KiroToolNameRegistry } { const tools = parsed.context.tools ?? []; const descriptionLimit = toolDescriptionLimit(parsed.modelId); + const rejectedSchemaKeys = normalizeKiroModelId(parsed.modelId).startsWith("claude-") ? NO_SCHEMA_KEYS : KIRO_REJECTED_SCHEMA_KEYS; // 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 +221,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 +252,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..9258740d5ec 100644 --- a/src/adapters/kiro/payload.ts +++ b/src/adapters/kiro/payload.ts @@ -199,8 +199,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), diff --git a/src/adapters/kiro/reasoning.ts b/src/adapters/kiro/reasoning.ts index c8f865b8d54..a1923df8c75 100644 --- a/src/adapters/kiro/reasoning.ts +++ b/src/adapters/kiro/reasoning.ts @@ -30,9 +30,17 @@ export const KIRO_NATIVE_EFFORT_FIELDS: Record { }, }; 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(""); }); @@ -1721,7 +1720,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", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-6-sol", "gpt-6-luna", "claude-opus-5", "claude-opus-5.5"]) { + for (const modelId of ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-6-sol", "gpt-6-luna", "claude-opus-5", "claude-opus-5.5", "claude-sonnet-5.5", "claude-opus-4.6", "claude-haiku-5.5"]) { await expect(createKiroAdapter(provider).buildRequest({ ...parsedWith([{ role: "user", content: "solve" }], undefined, modelId), options: { reasoning: "minimal" }, @@ -2025,3 +2024,166 @@ describe("kiro code-mode exec survives the catalog budget", () => { expect(serializedBytes).toBeLessThanOrEqual(MAX_KIRO_TOOL_CATALOG_BYTES); }); }); + +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): Promise { + const parsed = parsedWith([{ role: "user", content: "hi" }], [bashTool]); + 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"); + }); +}); + +describe("kiro tool schemas by model family", () => { + // Live probe on 2026-11-07: Claude models on Kiro accept every JSON Schema keyword the + // sanitizer used to strip, so the constraints reach the model. Other families keep the strip. + 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"], + }; + + 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("a Claude model receives the schema with every constraint", async () => { + for (const modelId of ["claude-opus-5.5", "claude-sonnet-5.5", "claude-haiku-5.5", "claude-opus-4.5"]) { + expect(await emittedSchema(modelId)).toEqual(constrained); + } + }); + + 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 } } }] }, + }], "claude-opus-5.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 }); + }); +}); 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/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."); + }); + }); +}); From 269650419ff57c1ede1f8bb59c98aeb4b88942fe Mon Sep 17 00:00:00 2001 From: svc-aibot-363 <271381517+svc-aibot-363@users.noreply.github.com> Date: Mon, 9 Nov 2026 13:26:30 +0100 Subject: [PATCH 02/17] fix(kiro): keep minimal emulated, Claude constraints in structured output, and document the tool-limit switches --- .../src/content/docs/reference/adapters.md | 9 +++++ src/adapters/kiro-tools.ts | 7 +++- src/adapters/kiro/payload.ts | 3 +- src/adapters/kiro/reasoning.ts | 19 ++++++++++ src/adapters/kiro/structured-output.ts | 4 +-- tests/providers/kiro/kiro-adapter.test.ts | 14 +++++++- .../kiro/kiro-structured-output.test.ts | 35 ++++++++++++++----- 7 files changed, 78 insertions(+), 13 deletions(-) diff --git a/docs-site/src/content/docs/reference/adapters.md b/docs-site/src/content/docs/reference/adapters.md index c3ca2a74702..74abcf936e5 100644 --- a/docs-site/src/content/docs/reference/adapters.md +++ b/docs-site/src/content/docs/reference/adapters.md @@ -575,6 +575,15 @@ 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. +- Strips the JSON Schema keywords Kiro's validator rejects (`pattern`, `minLength`, `maximum`, + `additionalProperties` and others) from tool and structured-output schemas, except for Claude + models, which accept them and keep the constraints. - 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 diff --git a/src/adapters/kiro-tools.ts b/src/adapters/kiro-tools.ts index f643f645921..9ecc231d8b6 100644 --- a/src/adapters/kiro-tools.ts +++ b/src/adapters/kiro-tools.ts @@ -73,6 +73,11 @@ const SCHEMA_LITERAL_KEYS = new Set(["const", "enum", "default", "examples"]); const NO_SCHEMA_KEYS: ReadonlySet = new Set(); +/** Schema keywords Kiro rejects for `modelId`: none for Claude models, the measured set otherwise. */ +export function kiroRejectedSchemaKeys(modelId: string): ReadonlySet { + return normalizeKiroModelId(modelId).startsWith("claude-") ? NO_SCHEMA_KEYS : 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 @@ -205,7 +210,7 @@ export function convertKiroToolContext( ): { tools: unknown[]; systemAdditions: string[]; nameMap: Map; registry: KiroToolNameRegistry } { const tools = parsed.context.tools ?? []; const descriptionLimit = toolDescriptionLimit(parsed.modelId); - const rejectedSchemaKeys = normalizeKiroModelId(parsed.modelId).startsWith("claude-") ? NO_SCHEMA_KEYS : KIRO_REJECTED_SCHEMA_KEYS; + 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; diff --git a/src/adapters/kiro/payload.ts b/src/adapters/kiro/payload.ts index 9258740d5ec..8f8de5dbd0d 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, @@ -487,7 +488,7 @@ export function buildKiroPayload( const effort = parsed.options.reasoning; const effortField = kiroNativeEffortField(parsed.modelId); if (effortField && effort && effort !== "none") { - if (!KIRO_NATIVE_EFFORTS.includes(effort)) { + if (!KIRO_NATIVE_EFFORTS.includes(effort) && !kiroEmulatesUnsupportedEffort(parsed.modelId)) { 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 a1923df8c75..094473a5d75 100644 --- a/src/adapters/kiro/reasoning.ts +++ b/src/adapters/kiro/reasoning.ts @@ -50,6 +50,17 @@ export const KIRO_NATIVE_EFFORTS = ["low", "medium", "high", "xhigh", "max"]; const KIRO_LUNA_TERRA_NATIVE_EFFORTS = new Set(["low", "medium", "high", "max"]); // Kiro rejects a native xhigh for these two (HTTP 400, live probe 2026-11-07); it stays emulated. const KIRO_NO_XHIGH_NATIVE_MODELS = new Set(["claude-opus-4.6", "claude-sonnet-4.6"]); +// These models used the emulated block for every effort before native effort was verified, so an +// effort outside the native ladder (`minimal`) keeps that block rather than becoming an error. +const KIRO_EMULATED_EXTRA_EFFORT_MODELS = new Set([ + "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", +]); export function kiroNativeEffortField( modelId: string, @@ -59,9 +70,17 @@ export function kiroNativeEffortField( if ((model === "gpt-5.6-luna" || model === "gpt-5.6-terra") && effort !== undefined && !KIRO_LUNA_TERRA_NATIVE_EFFORTS.has(effort)) return undefined; if (KIRO_NO_XHIGH_NATIVE_MODELS.has(model) && effort === "xhigh") return undefined; + if (KIRO_EMULATED_EXTRA_EFFORT_MODELS.has(model) && effort !== undefined && !KIRO_NATIVE_EFFORTS.includes(effort)) { + return undefined; + } return KIRO_NATIVE_EFFORT_FIELDS[model]; } +/** True when an effort outside the native ladder keeps the emulated path instead of failing. */ +export function kiroEmulatesUnsupportedEffort(modelId: string): boolean { + return KIRO_EMULATED_EXTRA_EFFORT_MODELS.has(normalizeKiroModelId(modelId)); +} + export function kiroReasoningMode(modelId: string, effort?: string): KiroReasoningMode { return kiroNativeEffortField(modelId, effort) ? "native" : "emulated"; } diff --git a/src/adapters/kiro/structured-output.ts b/src/adapters/kiro/structured-output.ts index eb68d03611b..4df8ae721ac 100644 --- a/src/adapters/kiro/structured-output.ts +++ b/src/adapters/kiro/structured-output.ts @@ -1,6 +1,6 @@ import type { AdapterEvent, OcxParsedRequest, OcxUsage } from "../../types"; import { KIRO_STRUCTURED_OUTPUT_TOOL_NAME } from "../kiro-constants"; -import { sanitizeKiroSchema } from "../kiro-tools"; +import { kiroRejectedSchemaKeys, sanitizeKiroSchema } from "../kiro-tools"; import { readKiroJson } from "./structured-output-json"; import { assertKiroStrictSchemaSupported, kiroStrictSchemaProblem, rewriteKiroWrappedRefs } from "./structured-output-schema"; @@ -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 diff --git a/tests/providers/kiro/kiro-adapter.test.ts b/tests/providers/kiro/kiro-adapter.test.ts index 33f0739fff8..06bf5537ceb 100644 --- a/tests/providers/kiro/kiro-adapter.test.ts +++ b/tests/providers/kiro/kiro-adapter.test.ts @@ -1720,13 +1720,25 @@ 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", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-6-sol", "gpt-6-luna", "claude-opus-5", "claude-opus-5.5", "claude-sonnet-5.5", "claude-opus-4.6", "claude-haiku-5.5"]) { + for (const modelId of ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-6-sol", "gpt-6-luna", "claude-opus-5", "claude-opus-5.5"]) { await expect(createKiroAdapter(provider).buildRequest({ ...parsedWith([{ role: "user", content: "solve" }], undefined, modelId), options: { reasoning: "minimal" }, })).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(); + expect(body.conversationState.currentMessage.userInputMessage.content).toContain("100"); + } + }); }); describe("kiro adapter — per-model context windows (kiro.dev/docs/models)", () => { diff --git a/tests/providers/kiro/kiro-structured-output.test.ts b/tests/providers/kiro/kiro-structured-output.test.ts index 970b4d78044..32803e87d69 100644 --- a/tests/providers/kiro/kiro-structured-output.test.ts +++ b/tests/providers/kiro/kiro-structured-output.test.ts @@ -116,11 +116,9 @@ 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. - 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. + // Sanitized the way the model's client tools are: Claude models keep every keyword. + expect(tools[0].toolSpecification.inputSchema.json).toEqual(titleSchema); + // 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 +134,39 @@ 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("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 }, }); + const claude = await payloadOf(structuredRequest({ type: "json_schema", name: "items", schema })); + 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 () => { From 8703f2521fa4c5ac7f226b01488e6bfad0377aa6 Mon Sep 17 00:00:00 2001 From: JUN Date: Tue, 10 Nov 2026 19:36:01 +0900 Subject: [PATCH 03/17] test(kiro): split the catalog tests and document the effort exceptions Move the catalog budget, description limit, tool-catalog nudge and schema policy cases out of kiro-adapter.test.ts into kiro-tool-catalog.test.ts. That file had reached 2,201 lines against its 2,050-line ratchet cap; the move keeps every existing case and assertion unchanged. Strengthen the minimal-effort regression to assert both halves of the emulated block (thinking_mode and the thinking instruction), not only the budget tag. Cover the reviewer's nudge concern: a non-Claude Kiro model reached from the Anthropic Messages surface drops the Codex nudge for the same surface-scoped reason a Claude model does, while the Codex surface keeps it. Document the native effort exceptions that actually ship: the newly enabled Claude models send output_config.effort, the 4.6 pair keeps xhigh emulated, and every newly enabled Claude model except Opus 5 / 5.5 keeps minimal emulated. The canonical page, all seven translation pages and structure/providers/kiro.md now agree with src/adapters/kiro/reasoning.ts. A model is named as emulated only when it is not among the native entries above it, so the paragraph no longer covers the GPT models it just described as native. The reference also states what disabling the tool budget does: no priority sort, no omission notice, every declared tool on the wire, and the upstream ValidationException as the failure mode for an oversized catalog. --- .../src/content/docs/fr/reference/adapters.md | 18 +- .../src/content/docs/ja/reference/adapters.md | 20 +- .../src/content/docs/ko/reference/adapters.md | 20 +- .../src/content/docs/reference/adapters.md | 30 ++- .../src/content/docs/ru/reference/adapters.md | 22 +- .../src/content/docs/tr/reference/adapters.md | 23 +- .../content/docs/zh-cn/reference/adapters.md | 15 +- .../content/docs/zh-tw/reference/adapters.md | 15 +- scripts/test-layout/layout.json | 2 +- structure/providers/kiro.md | 58 ++++- tests/fixtures/test-layout-expected.json | 1 + tests/providers/kiro/kiro-adapter.test.ts | 172 +------------- .../providers/kiro/kiro-tool-catalog.test.ts | 219 ++++++++++++++++++ 13 files changed, 407 insertions(+), 208 deletions(-) create mode 100644 tests/providers/kiro/kiro-tool-catalog.test.ts diff --git a/docs-site/src/content/docs/fr/reference/adapters.md b/docs-site/src/content/docs/fr/reference/adapters.md index d45e12b48a1..d849b977a47 100644 --- a/docs-site/src/content/docs/fr/reference/adapters.md +++ b/docs-site/src/content/docs/fr/reference/adapters.md @@ -142,11 +142,23 @@ 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`, +ainsi que les autres modèles 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 `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. +`gpt-5.6-sol` et les modèles GPT-6 (`gpt-6-sol`, `gpt-6-luna`, `gpt-6.1-sol`, `gpt-6.1-terra`) +conservent leurs niveaux natifs `low`, `medium`, `high`, `xhigh` et `max`. + +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`. Deux niveaux restent sur le bloc de réflexion émulé au lieu du champ natif : +`claude-opus-4.6` et `claude-sonnet-4.6` rejettent le `xhigh` natif, et tous les modèles de cette +liste sauf `claude-opus-5` et `claude-opus-5.5` considèrent `minimal` comme un niveau hors de +l’échelle native de Kiro : il conserve donc le bloc émulé au lieu de faire échouer la requête. Ces +deux modèles Opus 5 rejettent `minimal` comme une erreur de configuration, tout comme les modèles +GPT qui portent un effort natif. `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..e27ddae11e1 100644 --- a/docs-site/src/content/docs/ja/reference/adapters.md +++ b/docs-site/src/content/docs/ja/reference/adapters.md @@ -155,12 +155,22 @@ 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 系は `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-sol` と GPT-6 系(`gpt-6-sol`、`gpt-6-luna`、`gpt-6.1-sol`、`gpt-6.1-terra`)は +ネイティブの `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` で `output_config.effort` を受け付けます。次の 2 つの段階はネイティブフィールドではなく +エミュレーションの thinking ブロックを使います。`claude-opus-4.6` と `claude-sonnet-4.6` はネイティブの +`xhigh` を拒否し、`claude-opus-5` と `claude-opus-5.5` を除くこのリストの全モデルは `minimal` を +Kiro のネイティブ段階の外にある effort として扱うため、リクエストを失敗させずにエミュレーションの +ブロックを維持します。この 2 つの Opus 5 モデルは、ネイティブ effort を持つ GPT 系と同じく `minimal` を +設定エラーとして拒否します。`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..3228fe5bb70 100644 --- a/docs-site/src/content/docs/ko/reference/adapters.md +++ b/docs-site/src/content/docs/ko/reference/adapters.md @@ -188,12 +188,22 @@ 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 계열은 `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-sol`과 GPT-6 계열(`gpt-6-sol`, `gpt-6-luna`, `gpt-6.1-sol`, `gpt-6.1-terra`)은 +네이티브 `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`에 `output_config.effort`를 받아들입니다. 두 단계는 네이티브 필드 대신 +에뮬레이션 thinking 블록을 씁니다. `claude-opus-4.6`과 `claude-sonnet-4.6`은 네이티브 `xhigh`를 +거부하고, `claude-opus-5`와 `claude-opus-5.5`를 뺀 이 목록의 모든 모델은 `minimal`을 Kiro의 +네이티브 단계 밖 effort로 보아 요청을 실패시키는 대신 에뮬레이션 블록을 유지합니다. 이 두 Opus 5 +모델은 네이티브 effort를 쓰는 GPT 계열처럼 `minimal`을 설정 오류로 거부합니다. +`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 74abcf936e5..e5b0c1ab0a4 100644 --- a/docs-site/src/content/docs/reference/adapters.md +++ b/docs-site/src/content/docs/reference/adapters.md @@ -580,7 +580,12 @@ Use `liveModels: false` for a static catalog; Settings can switch between the tw 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. + 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 others) from tool and structured-output schemas, except for Claude models, which accept them and keep the constraints. @@ -722,12 +727,23 @@ 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. +and the other models 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 (`gpt-6-sol`, `gpt-6-luna`, `gpt-6.1-sol`, `gpt-6.1-terra`) keep their +native `low`, `medium`, `high`, `xhigh`, and `max` behavior. + +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`. Two rungs stay on the +emulated thinking block instead of the native field: `claude-opus-4.6` and `claude-sonnet-4.6` +reject native `xhigh`, and every model in that list except `claude-opus-5` and +`claude-opus-5.5` treats `minimal` as an effort outside Kiro's native ladder, so it keeps the +emulated block rather than failing the request. Those two Opus 5 models reject `minimal` as a +configuration error, as do the GPT models that carry native effort. `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..5c8cf5f6659 100644 --- a/docs-site/src/content/docs/ru/reference/adapters.md +++ b/docs-site/src/content/docs/ru/reference/adapters.md @@ -190,12 +190,22 @@ 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 использует `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` и модели GPT-6 (`gpt-6-sol`, `gpt-6-luna`, `gpt-6.1-sol`, +`gpt-6.1-terra`) сохраняют нативные `low`, `medium`, `high`, `xhigh` и `max`. + +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`. Два уровня остаются на эмулируемом блоке thinking вместо нативного поля: +`claude-opus-4.6` и `claude-sonnet-4.6` отклоняют нативный `xhigh`, а все модели этого списка, +кроме `claude-opus-5` и `claude-opus-5.5`, считают `minimal` уровнем вне нативной шкалы Kiro и +сохраняют эмулируемый блок вместо ошибки запроса. Эти две модели Opus 5 отклоняют `minimal` как +ошибку конфигурации, как и модели GPT с нативным effort. `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..f59fdfc2e46 100644 --- a/docs-site/src/content/docs/tr/reference/adapters.md +++ b/docs-site/src/content/docs/tr/reference/adapters.md @@ -279,12 +279,23 @@ 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 `additionalModelRequestFields.reasoning.effort`, `claude-opus-5` ile aşağıda +listelenen diğer modeller 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 (`gpt-6-sol`, +`gpt-6-luna`, `gpt-6.1-sol`, `gpt-6.1-terra`) yerel `low`, `medium`, `high`, `xhigh` ve `max` +davranışını 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 kabul eder. İki seviye yerel alan yerine öykünülen düşünme bloğunu +kullanır: `claude-opus-4.6` ve `claude-sonnet-4.6` yerel `xhigh` seviyesini reddeder ve bu listedeki +`claude-opus-5` ile `claude-opus-5.5` dışındaki tüm modeller `minimal` seviyesini Kiro'nun yerel +basamağı dışında bir çaba sayar; istek başarısız olmak yerine öykünülen bloğu korur. Bu iki Opus 5 +modeli, yerel çaba kullanan GPT modelleri gibi `minimal` seviyesini yapılandırma hatası olarak +reddeder. `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..bcd95239ad0 100644 --- a/docs-site/src/content/docs/zh-cn/reference/adapters.md +++ b/docs-site/src/content/docs/zh-cn/reference/adapters.md @@ -166,12 +166,21 @@ Kiro 的 assistant 文本本身没有可靠的回合结束标记,但终止的 ### Reasoning effort -GPT-5.6 系列使用 `additionalModelRequestFields.reasoning.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 选项并不代表原生支持。 +`gpt-5.6-sol` 和 GPT-6 系列(`gpt-6-sol`、`gpt-6-luna`、`gpt-6.1-sol`、`gpt-6.1-terra`) +保留原生 `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` 接受 `output_config.effort`。有两个档位改用模拟 thinking 块而不是原生字段: +`claude-opus-4.6` 和 `claude-sonnet-4.6` 拒绝原生 `xhigh`;该列表中除 `claude-opus-5` 和 +`claude-opus-5.5` 之外的所有模型都把 `minimal` 视为 Kiro 原生档位之外的 effort,因此保留模拟块而不会让请求失败。 +这两个 Opus 5 模型把 `minimal` 当作配置错误拒绝,与使用原生 effort 的 GPT 系列相同。 +`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..bf392bf66ae 100644 --- a/docs-site/src/content/docs/zh-tw/reference/adapters.md +++ b/docs-site/src/content/docs/zh-tw/reference/adapters.md @@ -146,12 +146,21 @@ Kiro 的 assistant 文字本身沒有可靠的回合結束標記,但終止的 ### Reasoning effort -GPT-5.6 系列使用 `additionalModelRequestFields.reasoning.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 選項不代表原生支援。 +`gpt-5.6-sol` 和 GPT-6 系列(`gpt-6-sol`、`gpt-6-luna`、`gpt-6.1-sol`、`gpt-6.1-terra`) +保留原生 `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` 接受 `output_config.effort`。有兩個檔位改用模擬 thinking 區塊而非原生欄位: +`claude-opus-4.6` 和 `claude-sonnet-4.6` 拒絕原生 `xhigh`;該清單中除 `claude-opus-5` 和 +`claude-opus-5.5` 以外的所有模型都把 `minimal` 視為 Kiro 原生檔位之外的 effort,因此保留模擬區塊而不會讓請求失敗。 +這兩個 Opus 5 模型把 `minimal` 當成設定錯誤拒絕,與使用原生 effort 的 GPT 系列相同。 +`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 62a7656f6db..f789943c7b3 100644 --- a/scripts/test-layout/layout.json +++ b/scripts/test-layout/layout.json @@ -1100,7 +1100,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/structure/providers/kiro.md b/structure/providers/kiro.md index 547ee8458e4..f427c4c62bf 100644 --- a/structure/providers/kiro.md +++ b/structure/providers/kiro.md @@ -133,6 +133,46 @@ 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, not per transport. `kiroRejectedSchemaKeys(modelId)` +returns an empty set for `claude-*` models — the 2026-11-07 probe accepted all 28 rejected +keywords there, one per request — and `KIRO_REJECTED_SCHEMA_KEYS` for every other family. 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: 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 @@ -290,10 +330,20 @@ 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's native effort ladder has no `minimal` +rung; the seven Claude models that gained native effort in #8451 used the emulated block for +every effort before, so `KIRO_EMULATED_EXTRA_EFFORT_MODELS` keeps that block for an effort +outside the ladder instead of turning it into a request error. `claude-opus-5` and +`claude-opus-5.5` are deliberately absent from that set: they had native effort before the +change, and `minimal` stays a configuration error for them, as it is for the GPT models with +native effort. 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 a1c60f8c977..292441635bd 100644 --- a/tests/fixtures/test-layout-expected.json +++ b/tests/fixtures/test-layout-expected.json @@ -978,6 +978,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 06bf5537ceb..29f0badfb1a 100644 --- a/tests/providers/kiro/kiro-adapter.test.ts +++ b/tests/providers/kiro/kiro-adapter.test.ts @@ -14,7 +14,7 @@ import { KIRO_TOOL_RESULT_CARRIER_MESSAGE, } from "../../../src/adapters/kiro-constants"; import { EMPTY_EXEC_OUTPUT_MESSAGE, FAILED_EXEC_OUTPUT_MESSAGE } from "../../../src/adapters/exec-tool-result-normalize"; -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 { MAX_KIRO_TOOL_CATALOG_BYTES, MAX_KIRO_TOOL_COUNT } from "../../../src/adapters/kiro-tools"; import { applyProviderConfigHints, buildCatalogEntries } from "../../../src/codex/catalog"; import { getValidAccessTokenSnapshot } from "../../../src/oauth"; import { saveCredential } from "../../../src/oauth/store"; @@ -1736,7 +1736,12 @@ describe("kiro adapter — native and emulated reasoning effort", () => { })).body); expect(body.additionalModelRequestFields).toBeUndefined(); - expect(body.conversationState.currentMessage.userInputMessage.content).toContain("100"); + // 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(""); } }); }); @@ -2036,166 +2041,3 @@ describe("kiro code-mode exec survives the catalog budget", () => { expect(serializedBytes).toBeLessThanOrEqual(MAX_KIRO_TOOL_CATALOG_BYTES); }); }); - -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): Promise { - const parsed = parsedWith([{ role: "user", content: "hi" }], [bashTool]); - 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"); - }); -}); - -describe("kiro tool schemas by model family", () => { - // Live probe on 2026-11-07: Claude models on Kiro accept every JSON Schema keyword the - // sanitizer used to strip, so the constraints reach the model. Other families keep the strip. - 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"], - }; - - 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("a Claude model receives the schema with every constraint", async () => { - for (const modelId of ["claude-opus-5.5", "claude-sonnet-5.5", "claude-haiku-5.5", "claude-opus-4.5"]) { - expect(await emittedSchema(modelId)).toEqual(constrained); - } - }); - - 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 } } }] }, - }], "claude-opus-5.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 }); - }); -}); 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..3cc5430fadb --- /dev/null +++ b/tests/providers/kiro/kiro-tool-catalog.test.ts @@ -0,0 +1,219 @@ +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", () => { + // Live probe on 2026-11-07: Claude models on Kiro accept every JSON Schema keyword the + // sanitizer used to strip, so the constraints reach the model. Other families keep the strip. + 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"], + }; + + 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("a Claude model receives the schema with every constraint", async () => { + for (const modelId of ["claude-opus-5.5", "claude-sonnet-5.5", "claude-haiku-5.5", "claude-opus-4.5"]) { + expect(await emittedSchema(modelId)).toEqual(constrained); + } + }); + + 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 } } }] }, + }], "claude-opus-5.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 }); + }); +}); From 6a939b5fb4bc66cfd8363373254d482c586a696e Mon Sep 17 00:00:00 2001 From: JUN Date: Wed, 11 Nov 2026 01:07:35 +0900 Subject: [PATCH 04/17] fix(kiro): scope schema exceptions to reported evidence --- .../src/content/docs/fr/reference/adapters.md | 23 ++++--- .../src/content/docs/ja/reference/adapters.md | 37 +++++++++--- .../src/content/docs/ko/reference/adapters.md | 36 ++++++++--- .../src/content/docs/reference/adapters.md | 28 +++++---- .../src/content/docs/ru/reference/adapters.md | 40 ++++++++++--- .../src/content/docs/tr/reference/adapters.md | 39 +++++++++--- .../content/docs/zh-cn/reference/adapters.md | 31 ++++++++-- .../content/docs/zh-tw/reference/adapters.md | 31 ++++++++-- src/adapters/kiro-tools.ts | 34 +++++++++-- src/adapters/kiro/payload.ts | 2 +- src/adapters/kiro/reasoning.ts | 22 +++++-- src/adapters/kiro/structured-output.ts | 14 +++-- structure/providers/kiro.md | 59 +++++++++++++----- .../kiro/kiro-structured-output.test.ts | 44 +++++++++++++- .../providers/kiro/kiro-tool-catalog.test.ts | 60 +++++++++++++++++-- 15 files changed, 402 insertions(+), 98 deletions(-) diff --git a/docs-site/src/content/docs/fr/reference/adapters.md b/docs-site/src/content/docs/fr/reference/adapters.md index d849b977a47..f11a8dddfbf 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. @@ -151,14 +153,19 @@ conservent leurs niveaux natifs `low`, `medium`, `high`, `xhigh` et `max`. 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`. Deux niveaux restent sur le bloc de réflexion émulé au lieu du champ natif : -`claude-opus-4.6` et `claude-sonnet-4.6` rejettent le `xhigh` natif, et tous les modèles de cette -liste sauf `claude-opus-5` et `claude-opus-5.5` considèrent `minimal` comme un niveau hors de -l’échelle native de Kiro : il conserve donc le bloc émulé au lieu de faire échouer la requête. Ces -deux modèles Opus 5 rejettent `minimal` comme une erreur de configuration, tout comme les modèles -GPT qui portent un effort natif. `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. +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` +nommant le modèle et le niveau. `ultra` n’atteint jamais Kiro : les chemins Responses et Messages +le réduisent tous deux à `max` avant que l’adaptateur ne construise la requête. +`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 e27ddae11e1..9db76483b97 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 を示す信頼できる区別がありません。ただし終端の @@ -164,13 +183,17 @@ GPT-5.6 系は `additionalModelRequestFields.reasoning.effort`、`claude-opus-5` 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` で `output_config.effort` を受け付けます。次の 2 つの段階はネイティブフィールドではなく -エミュレーションの thinking ブロックを使います。`claude-opus-4.6` と `claude-sonnet-4.6` はネイティブの -`xhigh` を拒否し、`claude-opus-5` と `claude-opus-5.5` を除くこのリストの全モデルは `minimal` を -Kiro のネイティブ段階の外にある effort として扱うため、リクエストを失敗させずにエミュレーションの -ブロックを維持します。この 2 つの Opus 5 モデルは、ネイティブ effort を持つ GPT 系と同じく `minimal` を -設定エラーとして拒否します。`claude-haiku-4.5`、`claude-opus-4.5`、`claude-sonnet-4.5` と、 -上記にない Kiro モデルはすべての effort でエミュレーションを使います。effort の選択肢だけではネイティブ対応を意味しません。 +`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` を +モデルと effort を示す 400 `invalid_request_error` で拒否します。`ultra` は Kiro に届きません。 +Responses と Messages のどちらの経路も、アダプターがリクエストを組み立てる前に `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/ko/reference/adapters.md b/docs-site/src/content/docs/ko/reference/adapters.md index 3228fe5bb70..906c27920c9 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의 어시스턴트 텍스트에는 그 자체로 턴 종료를 알리는 신뢰할 만한 구분이 없습니다. 다만 종단 @@ -197,13 +215,17 @@ GPT-5.6 계열은 `additionalModelRequestFields.reasoning.effort`를, `claude-op 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`에 `output_config.effort`를 받아들입니다. 두 단계는 네이티브 필드 대신 -에뮬레이션 thinking 블록을 씁니다. `claude-opus-4.6`과 `claude-sonnet-4.6`은 네이티브 `xhigh`를 -거부하고, `claude-opus-5`와 `claude-opus-5.5`를 뺀 이 목록의 모든 모델은 `minimal`을 Kiro의 -네이티브 단계 밖 effort로 보아 요청을 실패시키는 대신 에뮬레이션 블록을 유지합니다. 이 두 Opus 5 -모델은 네이티브 effort를 쓰는 GPT 계열처럼 `minimal`을 설정 오류로 거부합니다. -`claude-haiku-4.5`, `claude-opus-4.5`, `claude-sonnet-4.5`와 위에 없는 Kiro 모델은 모든 effort에서 -에뮬레이션을 씁니다. 조절 항목이 있다고 네이티브 지원을 뜻하지는 않습니다. +`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`을 모델과 effort를 +밝히는 400 `invalid_request_error`로 거부합니다. `ultra`는 Kiro에 도달하지 않습니다. Responses와 +Messages 경로 모두 어댑터가 요청을 만들기 전에 `max`로 낮춥니다. +`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 e5b0c1ab0a4..d419bf0fbf7 100644 --- a/docs-site/src/content/docs/reference/adapters.md +++ b/docs-site/src/content/docs/reference/adapters.md @@ -587,8 +587,12 @@ Use `liveModels: false` for a static catalog; Settings can switch between the tw 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 others) from tool and structured-output schemas, except for Claude - models, which accept them and keep the constraints. + `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 the PR does not report them. 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 @@ -736,14 +740,18 @@ native `low`, `medium`, `high`, `xhigh`, and `max` behavior. 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`. Two rungs stay on the -emulated thinking block instead of the native field: `claude-opus-4.6` and `claude-sonnet-4.6` -reject native `xhigh`, and every model in that list except `claude-opus-5` and -`claude-opus-5.5` treats `minimal` as an effort outside Kiro's native ladder, so it keeps the -emulated block rather than failing the request. Those two Opus 5 models reject `minimal` as a -configuration error, as do the GPT models that carry native effort. `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. +`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` naming the model and the effort. `ultra` never +reaches Kiro: the Responses and Messages paths both reduce it to `max` before the adapter builds +a request. `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 5c8cf5f6659..45be67e2648 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 сам по себе не даёт надёжного признака конца хода. Однако завершающий @@ -199,13 +220,18 @@ incomplete. `TOOL_USE` без фактического вызова инстру 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`. Два уровня остаются на эмулируемом блоке thinking вместо нативного поля: -`claude-opus-4.6` и `claude-sonnet-4.6` отклоняют нативный `xhigh`, а все модели этого списка, -кроме `claude-opus-5` и `claude-opus-5.5`, считают `minimal` уровнем вне нативной шкалы Kiro и -сохраняют эмулируемый блок вместо ошибки запроса. Эти две модели Opus 5 отклоняют `minimal` как -ошибку конфигурации, как и модели GPT с нативным effort. `claude-haiku-4.5`, `claude-opus-4.5`, -`claude-sonnet-4.5` и любая не перечисленная выше модель Kiro используют эмуляцию на каждом -уровне; наличие настройки effort не доказывает нативную поддержку. +и `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` с указанием модели и уровня. `ultra` до Kiro не доходит: и путь +Responses, и путь Messages сводят его к `max` до того, как адаптер построит запрос. +`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 f59fdfc2e46..74607ac2aef 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ı o 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 @@ -289,13 +309,18 @@ davranışını 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 kabul eder. İki seviye yerel alan yerine öykünülen düşünme bloğunu -kullanır: `claude-opus-4.6` ve `claude-sonnet-4.6` yerel `xhigh` seviyesini reddeder ve bu listedeki -`claude-opus-5` ile `claude-opus-5.5` dışındaki tüm modeller `minimal` seviyesini Kiro'nun yerel -basamağı dışında bir çaba sayar; istek başarısız olmak yerine öykünülen bloğu korur. Bu iki Opus 5 -modeli, yerel çaba kullanan GPT modelleri gibi `minimal` seviyesini yapılandırma hatası olarak -reddeder. `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. +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 modeli ve seviyeyi belirten 400 `invalid_request_error` ile reddeder. +`ultra` Kiro'ya hiç ulaşmaz: hem Responses hem Messages yolu, adaptör isteği kurmadan önce onu +`max` seviyesine indirir. `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 bcd95239ad0..c611ab9da01 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`。 @@ -175,12 +190,16 @@ GPT-5.6 系列使用 `additionalModelRequestFields.reasoning.effort`,`claude-o 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` 接受 `output_config.effort`。有两个档位改用模拟 thinking 块而不是原生字段: -`claude-opus-4.6` 和 `claude-sonnet-4.6` 拒绝原生 `xhigh`;该列表中除 `claude-opus-5` 和 -`claude-opus-5.5` 之外的所有模型都把 `minimal` 视为 Kiro 原生档位之外的 effort,因此保留模拟块而不会让请求失败。 -这两个 Opus 5 模型把 `minimal` 当作配置错误拒绝,与使用原生 effort 的 GPT 系列相同。 -`claude-haiku-4.5`、`claude-opus-4.5`、`claude-sonnet-4.5` 以及上面未列出的 Kiro 模型在每个 effort 上 -都使用模拟推理;提供 effort 选项并不代表原生支持。 +`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`,并在错误中标明模型和 +effort。`ultra` 永远不会到达 Kiro:Responses 和 Messages 路径都会在适配器构建请求前把它降为 +`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 bf392bf66ae..733fbb70e68 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 @@ -155,12 +170,16 @@ GPT-5.6 系列使用 `additionalModelRequestFields.reasoning.effort`,`claude-o 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` 接受 `output_config.effort`。有兩個檔位改用模擬 thinking 區塊而非原生欄位: -`claude-opus-4.6` 和 `claude-sonnet-4.6` 拒絕原生 `xhigh`;該清單中除 `claude-opus-5` 和 -`claude-opus-5.5` 以外的所有模型都把 `minimal` 視為 Kiro 原生檔位之外的 effort,因此保留模擬區塊而不會讓請求失敗。 -這兩個 Opus 5 模型把 `minimal` 當成設定錯誤拒絕,與使用原生 effort 的 GPT 系列相同。 -`claude-haiku-4.5`、`claude-opus-4.5`、`claude-sonnet-4.5` 以及上面未列出的 Kiro 模型在每個 effort 上 -都使用模擬推理;提供 effort 選項不代表原生支援。 +`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`,並在錯誤中標明模型和 +effort。`ultra` 永遠不會到達 Kiro:Responses 和 Messages 路徑都會在適配器建置請求前把它降為 +`max`。`claude-haiku-4.5`、`claude-opus-4.5`、`claude-sonnet-4.5` 以及上面未列出的 Kiro +模型在每個 effort 上都使用模擬推理;提供 effort 選項不代表原生支援。 ## `cursor` diff --git a/src/adapters/kiro-tools.ts b/src/adapters/kiro-tools.ts index 9ecc231d8b6..f1bc5412db0 100644 --- a/src/adapters/kiro-tools.ts +++ b/src/adapters/kiro-tools.ts @@ -21,8 +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. -// Claude models accept every one of these keys (live probe on 2026-11-07, one key per request -// plus a full Claude Code catalog), so their schemas keep the constraints the model needs. +// 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", @@ -73,9 +73,35 @@ const SCHEMA_LITERAL_KEYS = new Set(["const", "enum", "default", "examples"]); const NO_SCHEMA_KEYS: ReadonlySet = new Set(); -/** Schema keywords Kiro rejects for `modelId`: none for Claude models, the measured set otherwise. */ +/** + * 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 accepted on opus-4.5, with if/then/else grouped +// in its count of 28. 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 normalizeKiroModelId(modelId).startsWith("claude-") ? NO_SCHEMA_KEYS : KIRO_REJECTED_SCHEMA_KEYS; + return KIRO_SCHEMA_KEY_EXCEPTIONS.get(normalizeKiroModelId(modelId)) ?? KIRO_REJECTED_SCHEMA_KEYS; } function sanitizeSchemaMap(value: unknown, rejected: ReadonlySet = KIRO_REJECTED_SCHEMA_KEYS): unknown { diff --git a/src/adapters/kiro/payload.ts b/src/adapters/kiro/payload.ts index 8f8de5dbd0d..382071dac78 100644 --- a/src/adapters/kiro/payload.ts +++ b/src/adapters/kiro/payload.ts @@ -488,7 +488,7 @@ export function buildKiroPayload( const effort = parsed.options.reasoning; const effortField = kiroNativeEffortField(parsed.modelId); if (effortField && effort && effort !== "none") { - if (!KIRO_NATIVE_EFFORTS.includes(effort) && !kiroEmulatesUnsupportedEffort(parsed.modelId)) { + 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 094473a5d75..ee5e1131a4e 100644 --- a/src/adapters/kiro/reasoning.ts +++ b/src/adapters/kiro/reasoning.ts @@ -31,7 +31,9 @@ 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. @@ -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/structure/providers/kiro.md b/structure/providers/kiro.md index f427c4c62bf..4dba25ce890 100644 --- a/structure/providers/kiro.md +++ b/structure/providers/kiro.md @@ -157,13 +157,31 @@ intact and the upstream `ValidationException` becomes the failure mode instead o request. The default remains bounded because that is the shape a fresh Kiro account is measured to accept. -Schema sanitization is chosen per model, not per transport. `kiroRejectedSchemaKeys(modelId)` -returns an empty set for `claude-*` models — the 2026-11-07 probe accepted all 28 rejected -keywords there, one per request — and `KIRO_REJECTED_SCHEMA_KEYS` for every other family. Both -the client-tool path and the structured-output tool (`kiroStructuredOutputSpec` in +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 the rejected keywords (counting `if/then/else` +as one item in its total of 28), so its exception is the empty set. 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-4.8`, `claude-opus-4.7`, +`claude-opus-4.6`, `claude-sonnet-5`, `claude-sonnet-4.6`, `claude-haiku-5.5`, +`claude-haiku-4.5`, `claude-sonnet-4.0`) 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: it is a wire requirement, not a constraint strip. +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` @@ -192,7 +210,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 @@ -337,13 +356,25 @@ individual rung on the emulated path. Luna and terra send only `low`, `medium`, `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's native effort ladder has no `minimal` -rung; the seven Claude models that gained native effort in #8451 used the emulated block for -every effort before, so `KIRO_EMULATED_EXTRA_EFFORT_MODELS` keeps that block for an effort -outside the ladder instead of turning it into a request error. `claude-opus-5` and -`claude-opus-5.5` are deliberately absent from that set: they had native effort before the -change, and `minimal` stays a configuration error for them, as it is for the GPT models with -native effort. A future shared effort rung does not widen any of these allowlists. +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. `minimal` is the only +outside-ladder value that reaches a Kiro request: the Responses parser folds `ultra` to `max`, as +does the Messages translator, 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. + +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` naming the model and the effort. 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/providers/kiro/kiro-structured-output.test.ts b/tests/providers/kiro/kiro-structured-output.test.ts index 32803e87d69..474d0f3d438 100644 --- a/tests/providers/kiro/kiro-structured-output.test.ts +++ b/tests/providers/kiro/kiro-structured-output.test.ts @@ -116,8 +116,10 @@ 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 the model's client tools are: Claude models keep every keyword. - expect(tools[0].toolSpecification.inputSchema.json).toEqual(titleSchema); + // 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"], + }); // 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)); @@ -146,6 +148,36 @@ describe("Kiro structured-output payload", () => { }); }); + 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" } }], @@ -157,7 +189,13 @@ describe("Kiro structured-output payload", () => { required: ["value"], $defs: { item }, }); - const claude = await payloadOf(structuredRequest({ type: "json_schema", name: "items", schema })); + // 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({ diff --git a/tests/providers/kiro/kiro-tool-catalog.test.ts b/tests/providers/kiro/kiro-tool-catalog.test.ts index 3cc5430fadb..b726d6ff6e2 100644 --- a/tests/providers/kiro/kiro-tool-catalog.test.ts +++ b/tests/providers/kiro/kiro-tool-catalog.test.ts @@ -168,8 +168,8 @@ describe("kiro tool-catalog nudge and the Anthropic Messages surface", () => { }); describe("kiro tool schemas by model family", () => { - // Live probe on 2026-11-07: Claude models on Kiro accept every JSON Schema keyword the - // sanitizer used to strip, so the constraints reach the model. Other families keep the strip. + // 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", @@ -181,6 +181,25 @@ describe("kiro tool schemas by model family", () => { }, 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( @@ -189,9 +208,22 @@ describe("kiro tool schemas by model family", () => { return JSON.parse(body).conversationState.currentMessage.userInputMessage.userInputMessageContext.tools[0].toolSpecification.inputSchema.json; } - test("a Claude model receives the schema with every constraint", async () => { - for (const modelId of ["claude-opus-5.5", "claude-sonnet-5.5", "claude-haiku-5.5", "claude-opus-4.5"]) { - expect(await emittedSchema(modelId)).toEqual(constrained); + 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); } }); @@ -209,7 +241,9 @@ describe("kiro tool schemas by model family", () => { 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 } } }] }, - }], "claude-opus-5.5"), + // 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"); @@ -217,3 +251,17 @@ describe("kiro tool schemas by model family", () => { 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 reachable outside-ladder value is `minimal`; `ultra` is folded to `max` upstream. 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"`); + } + }); +}); From 9f6a2e69f4327e92cf458619e9609a9d692a3ac3 Mon Sep 17 00:00:00 2001 From: JUN Date: Thu, 12 Nov 2026 16:47:23 +0900 Subject: [PATCH 05/17] docs(kiro): correct the GPT-6 effort field and the unprobed Claude roster The adapters reference assigned the GPT-6 models to `output_config.effort` by grouping them with the Claude ids behind a "the other models listed below" clause. `src/adapters/kiro/reasoning.ts` maps `gpt-6-sol`, `gpt-6-luna`, `gpt-6.1-sol` and `gpt-6.1-terra` to `reasoning.effort`, the same native field as the GPT-5.6 family; only the nine probed Claude ids use `output_config.effort`. Each locale now names the GPT-6 family in the `reasoning.effort` sentence and keeps `output_config.effort` for the Claude list. `structure/providers/kiro.md` listed the unprobed Claude ids for schema keyword sanitization but omitted `claude-opus-5` and `claude-sonnet-4.5`, both present in `KIRO_MODELS` and absent from `KIRO_SCHEMA_KEY_EXCEPTIONS`. The roster now names all ten unprobed ids. Docs only: no source, test, or behavior change. --- .../src/content/docs/fr/reference/adapters.md | 15 ++++++++------- .../src/content/docs/ja/reference/adapters.md | 9 +++++---- .../src/content/docs/ko/reference/adapters.md | 9 +++++---- .../src/content/docs/reference/adapters.md | 9 +++++---- .../src/content/docs/ru/reference/adapters.md | 9 +++++---- .../src/content/docs/tr/reference/adapters.md | 10 +++++----- .../content/docs/zh-cn/reference/adapters.md | 7 ++++--- .../content/docs/zh-tw/reference/adapters.md | 7 ++++--- structure/providers/kiro.md | 18 ++++++++++-------- 9 files changed, 51 insertions(+), 42 deletions(-) diff --git a/docs-site/src/content/docs/fr/reference/adapters.md b/docs-site/src/content/docs/fr/reference/adapters.md index f11a8dddfbf..d4609195d62 100644 --- a/docs-site/src/content/docs/fr/reference/adapters.md +++ b/docs-site/src/content/docs/fr/reference/adapters.md @@ -143,13 +143,14 @@ 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` -ainsi que les autres modèles 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 (`gpt-6-sol`, `gpt-6-luna`, `gpt-6.1-sol`, `gpt-6.1-terra`) -conservent leurs niveaux natifs `low`, `medium`, `high`, `xhigh` et `max`. +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` diff --git a/docs-site/src/content/docs/ja/reference/adapters.md b/docs-site/src/content/docs/ja/reference/adapters.md index 9db76483b97..5bded21839c 100644 --- a/docs-site/src/content/docs/ja/reference/adapters.md +++ b/docs-site/src/content/docs/ja/reference/adapters.md @@ -174,12 +174,13 @@ filtered incomplete になります。実際のツール呼び出しを伴わな ### Reasoning effort -GPT-5.6 系は `additionalModelRequestFields.reasoning.effort`、`claude-opus-5` と -以下に挙げるその他のモデルは `additionalModelRequestFields.output_config.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 系(`gpt-6-sol`、`gpt-6-luna`、`gpt-6.1-sol`、`gpt-6.1-terra`)は -ネイティブの `low`、`medium`、`high`、`xhigh`、`max` を維持します。 +`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`、 diff --git a/docs-site/src/content/docs/ko/reference/adapters.md b/docs-site/src/content/docs/ko/reference/adapters.md index 906c27920c9..abd6ca64c9a 100644 --- a/docs-site/src/content/docs/ko/reference/adapters.md +++ b/docs-site/src/content/docs/ko/reference/adapters.md @@ -206,12 +206,13 @@ commentary로 유지하고 비공개 완료 툴을 한 번 검증합니다. ### Reasoning effort -GPT-5.6 계열은 `additionalModelRequestFields.reasoning.effort`를, `claude-opus-5`와 -아래에 나열한 다른 모델은 `additionalModelRequestFields.output_config.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 계열(`gpt-6-sol`, `gpt-6-luna`, `gpt-6.1-sol`, `gpt-6.1-terra`)은 -네이티브 `low`, `medium`, `high`, `xhigh`, `max`를 그대로 사용합니다. +`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`, diff --git a/docs-site/src/content/docs/reference/adapters.md b/docs-site/src/content/docs/reference/adapters.md index 920083aafff..2d18bd3fc74 100644 --- a/docs-site/src/content/docs/reference/adapters.md +++ b/docs-site/src/content/docs/reference/adapters.md @@ -732,13 +732,14 @@ 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` -and the other models listed below use `additionalModelRequestFields.output_config.effort`. +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 (`gpt-6-sol`, `gpt-6-luna`, `gpt-6.1-sol`, `gpt-6.1-terra`) keep their -native `low`, `medium`, `high`, `xhigh`, and `max` behavior. +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`, diff --git a/docs-site/src/content/docs/ru/reference/adapters.md b/docs-site/src/content/docs/ru/reference/adapters.md index 45be67e2648..ab5d7c58734 100644 --- a/docs-site/src/content/docs/ru/reference/adapters.md +++ b/docs-site/src/content/docs/ru/reference/adapters.md @@ -211,12 +211,13 @@ incomplete. `TOOL_USE` без фактического вызова инстру ### Reasoning effort -Семейство GPT-5.6 использует `additionalModelRequestFields.reasoning.effort`, а `claude-opus-5` -и остальные перечисленные ниже модели — `additionalModelRequestFields.output_config.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 (`gpt-6-sol`, `gpt-6-luna`, `gpt-6.1-sol`, -`gpt-6.1-terra`) сохраняют нативные `low`, `medium`, `high`, `xhigh` и `max`. +уровень не проверен. `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` diff --git a/docs-site/src/content/docs/tr/reference/adapters.md b/docs-site/src/content/docs/tr/reference/adapters.md index 74607ac2aef..2b1b1844821 100644 --- a/docs-site/src/content/docs/tr/reference/adapters.md +++ b/docs-site/src/content/docs/tr/reference/adapters.md @@ -299,13 +299,13 @@ 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` ile aşağıda -listelenen diğer modeller ise `additionalModelRequestFields.output_config.effort` alanını kullanır. +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 (`gpt-6-sol`, -`gpt-6-luna`, `gpt-6.1-sol`, `gpt-6.1-terra`) yerel `low`, `medium`, `high`, `xhigh` ve `max` -davranışını sürdürür. +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` 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 c611ab9da01..eb799512b71 100644 --- a/docs-site/src/content/docs/zh-cn/reference/adapters.md +++ b/docs-site/src/content/docs/zh-cn/reference/adapters.md @@ -181,12 +181,13 @@ 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` 和 GPT-6 系列(`gpt-6-sol`、`gpt-6-luna`、`gpt-6.1-sol`、`gpt-6.1-terra`) -保留原生 `low`、`medium`、`high`、`xhigh`、`max`。 +`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` 和 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 733fbb70e68..dfa7e487aa1 100644 --- a/docs-site/src/content/docs/zh-tw/reference/adapters.md +++ b/docs-site/src/content/docs/zh-tw/reference/adapters.md @@ -161,12 +161,13 @@ 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` 和 GPT-6 系列(`gpt-6-sol`、`gpt-6-luna`、`gpt-6.1-sol`、`gpt-6.1-terra`) -保留原生 `low`、`medium`、`high`、`xhigh`、`max`。 +`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` 和 diff --git a/structure/providers/kiro.md b/structure/providers/kiro.md index 4dba25ce890..1669bebba38 100644 --- a/structure/providers/kiro.md +++ b/structure/providers/kiro.md @@ -168,14 +168,16 @@ as one item in its total of 28), so its exception is the empty set. It reports a 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-4.8`, `claude-opus-4.7`, -`claude-opus-4.6`, `claude-sonnet-5`, `claude-sonnet-4.6`, `claude-haiku-5.5`, -`claude-haiku-4.5`, `claude-sonnet-4.0`) 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. +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 From 5f19c53ceee84970e52f3fbb551b8b8498438fdb Mon Sep 17 00:00:00 2001 From: JUN Date: Thu, 12 Nov 2026 18:27:58 +0900 Subject: [PATCH 06/17] docs(kiro): state the measured rejected-key count instead of the report's 28 The comment in `src/adapters/kiro-tools.ts` and the schema-sanitization paragraph in `structure/providers/kiro.md` described the opus-4.5 probe as grouping `if/then/else` in a total of 28. That number is the report's stated total, not the measured set: `KIRO_REJECTED_SCHEMA_KEYS` holds 29 keys, and PR #8451's enumerated list is 27 whitespace-separated items that expand to exactly those 29 once `if/then/else` is read as three keys. The list covers the whole set, so the exception stays the empty set. Both places now separate the two facts: the list enumerates 27 items and names all 29 keys, and the report states that total as 28, one fewer than the keys its own list prints. Nothing claims a probe was performed that the report does not describe, and no unmeasured key is implied to be accepted. The eight docs locales already state 29 keys (four spell out the number, four write four named keys plus 25 others) and are unchanged. Docs and comment only: no source, test, or behavior change. --- src/adapters/kiro-tools.ts | 7 +++++-- structure/providers/kiro.md | 11 +++++++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/adapters/kiro-tools.ts b/src/adapters/kiro-tools.ts index f1bc5412db0..9beaec58bd6 100644 --- a/src/adapters/kiro-tools.ts +++ b/src/adapters/kiro-tools.ts @@ -84,8 +84,11 @@ function rejectAllExcept(accepted: readonly string[]): ReadonlySet { return new Set([...KIRO_REJECTED_SCHEMA_KEYS].filter(key => !keep.has(key))); } -// PR #8451 reports the rejected keys accepted on opus-4.5, with if/then/else grouped -// in its count of 28. Its two 5.5 probes explicitly name only pattern and maximum. +// 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], diff --git a/structure/providers/kiro.md b/structure/providers/kiro.md index 1669bebba38..503ba1befcf 100644 --- a/structure/providers/kiro.md +++ b/structure/providers/kiro.md @@ -163,10 +163,13 @@ Schema sanitization is chosen per model id and per keyword, not per family or tr 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 the rejected keywords (counting `if/then/else` -as one item in its total of 28), so its exception is the empty set. 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. +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`, From 4878867f08111a43e8983793c01955afa997456c Mon Sep 17 00:00:00 2001 From: svc-aibot-363 <271381517+svc-aibot-363@users.noreply.github.com> Date: Mon, 9 Nov 2026 12:54:04 +0100 Subject: [PATCH 07/17] fix(kiro): send the full tool contract and native effort to Kiro Claude models Refs #8306 (cherry picked from commit 584b46a37369c83928e9e3fed7fc9ffeb1d9606e) --- src/adapters/kiro-tools.ts | 42 ++-- src/adapters/kiro/payload.ts | 5 +- src/adapters/kiro/reasoning.ts | 13 +- src/providers/kiro-models.ts | 8 +- src/server/responses/request-prepare.ts | 1 + src/types/request.ts | 2 + tests/providers/kiro/kiro-adapter.test.ts | 184 ++++++++++++++++-- .../kiro/kiro-reasoning-roundtrip.test.ts | 49 +++++ .../server-kiro-structured-output-e2e.test.ts | 41 ++++ 9 files changed, 311 insertions(+), 34 deletions(-) diff --git a/src/adapters/kiro-tools.ts b/src/adapters/kiro-tools.ts index 4466ddf7133..f643f645921 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. +// Claude models accept every one of these keys (live probe on 2026-11-07, one key per request +// plus a full Claude Code catalog), so their schemas keep the constraints the model needs. const KIRO_REJECTED_SCHEMA_KEYS = new Set([ "additionalProperties", "pattern", @@ -65,28 +71,30 @@ 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(); + +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 +117,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 +132,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 +155,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 +205,7 @@ export function convertKiroToolContext( ): { tools: unknown[]; systemAdditions: string[]; nameMap: Map; registry: KiroToolNameRegistry } { const tools = parsed.context.tools ?? []; const descriptionLimit = toolDescriptionLimit(parsed.modelId); + const rejectedSchemaKeys = normalizeKiroModelId(parsed.modelId).startsWith("claude-") ? NO_SCHEMA_KEYS : KIRO_REJECTED_SCHEMA_KEYS; // 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 +221,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 +252,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..9258740d5ec 100644 --- a/src/adapters/kiro/payload.ts +++ b/src/adapters/kiro/payload.ts @@ -199,8 +199,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), diff --git a/src/adapters/kiro/reasoning.ts b/src/adapters/kiro/reasoning.ts index c8f865b8d54..a1923df8c75 100644 --- a/src/adapters/kiro/reasoning.ts +++ b/src/adapters/kiro/reasoning.ts @@ -30,9 +30,17 @@ export const KIRO_NATIVE_EFFORT_FIELDS: Record { }, }; 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(""); }); @@ -1721,7 +1720,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", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-6-sol", "gpt-6-luna", "claude-opus-5", "claude-opus-5.5"]) { + for (const modelId of ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-6-sol", "gpt-6-luna", "claude-opus-5", "claude-opus-5.5", "claude-sonnet-5.5", "claude-opus-4.6", "claude-haiku-5.5"]) { await expect(createKiroAdapter(provider).buildRequest({ ...parsedWith([{ role: "user", content: "solve" }], undefined, modelId), options: { reasoning: "minimal" }, @@ -2025,3 +2024,166 @@ describe("kiro code-mode exec survives the catalog budget", () => { expect(serializedBytes).toBeLessThanOrEqual(MAX_KIRO_TOOL_CATALOG_BYTES); }); }); + +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): Promise { + const parsed = parsedWith([{ role: "user", content: "hi" }], [bashTool]); + 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"); + }); +}); + +describe("kiro tool schemas by model family", () => { + // Live probe on 2026-11-07: Claude models on Kiro accept every JSON Schema keyword the + // sanitizer used to strip, so the constraints reach the model. Other families keep the strip. + 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"], + }; + + 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("a Claude model receives the schema with every constraint", async () => { + for (const modelId of ["claude-opus-5.5", "claude-sonnet-5.5", "claude-haiku-5.5", "claude-opus-4.5"]) { + expect(await emittedSchema(modelId)).toEqual(constrained); + } + }); + + 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 } } }] }, + }], "claude-opus-5.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 }); + }); +}); 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/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."); + }); + }); +}); From 606710352fea8a77293ee1a03407a6c110927add Mon Sep 17 00:00:00 2001 From: svc-aibot-363 <271381517+svc-aibot-363@users.noreply.github.com> Date: Mon, 9 Nov 2026 13:26:30 +0100 Subject: [PATCH 08/17] fix(kiro): keep minimal emulated, Claude constraints in structured output, and document the tool-limit switches (cherry picked from commit 269650419ff57c1ede1f8bb59c98aeb4b88942fe) --- .../src/content/docs/reference/adapters.md | 9 +++++ src/adapters/kiro-tools.ts | 7 +++- src/adapters/kiro/payload.ts | 3 +- src/adapters/kiro/reasoning.ts | 19 ++++++++++ src/adapters/kiro/structured-output.ts | 4 +-- tests/providers/kiro/kiro-adapter.test.ts | 14 +++++++- .../kiro/kiro-structured-output.test.ts | 35 ++++++++++++++----- 7 files changed, 78 insertions(+), 13 deletions(-) diff --git a/docs-site/src/content/docs/reference/adapters.md b/docs-site/src/content/docs/reference/adapters.md index b7d038c1854..ff837de8b54 100644 --- a/docs-site/src/content/docs/reference/adapters.md +++ b/docs-site/src/content/docs/reference/adapters.md @@ -577,6 +577,15 @@ 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. +- Strips the JSON Schema keywords Kiro's validator rejects (`pattern`, `minLength`, `maximum`, + `additionalProperties` and others) from tool and structured-output schemas, except for Claude + models, which accept them and keep the constraints. - 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 diff --git a/src/adapters/kiro-tools.ts b/src/adapters/kiro-tools.ts index f643f645921..9ecc231d8b6 100644 --- a/src/adapters/kiro-tools.ts +++ b/src/adapters/kiro-tools.ts @@ -73,6 +73,11 @@ const SCHEMA_LITERAL_KEYS = new Set(["const", "enum", "default", "examples"]); const NO_SCHEMA_KEYS: ReadonlySet = new Set(); +/** Schema keywords Kiro rejects for `modelId`: none for Claude models, the measured set otherwise. */ +export function kiroRejectedSchemaKeys(modelId: string): ReadonlySet { + return normalizeKiroModelId(modelId).startsWith("claude-") ? NO_SCHEMA_KEYS : 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 @@ -205,7 +210,7 @@ export function convertKiroToolContext( ): { tools: unknown[]; systemAdditions: string[]; nameMap: Map; registry: KiroToolNameRegistry } { const tools = parsed.context.tools ?? []; const descriptionLimit = toolDescriptionLimit(parsed.modelId); - const rejectedSchemaKeys = normalizeKiroModelId(parsed.modelId).startsWith("claude-") ? NO_SCHEMA_KEYS : KIRO_REJECTED_SCHEMA_KEYS; + 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; diff --git a/src/adapters/kiro/payload.ts b/src/adapters/kiro/payload.ts index 9258740d5ec..8f8de5dbd0d 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, @@ -487,7 +488,7 @@ export function buildKiroPayload( const effort = parsed.options.reasoning; const effortField = kiroNativeEffortField(parsed.modelId); if (effortField && effort && effort !== "none") { - if (!KIRO_NATIVE_EFFORTS.includes(effort)) { + if (!KIRO_NATIVE_EFFORTS.includes(effort) && !kiroEmulatesUnsupportedEffort(parsed.modelId)) { 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 a1923df8c75..094473a5d75 100644 --- a/src/adapters/kiro/reasoning.ts +++ b/src/adapters/kiro/reasoning.ts @@ -50,6 +50,17 @@ export const KIRO_NATIVE_EFFORTS = ["low", "medium", "high", "xhigh", "max"]; const KIRO_LUNA_TERRA_NATIVE_EFFORTS = new Set(["low", "medium", "high", "max"]); // Kiro rejects a native xhigh for these two (HTTP 400, live probe 2026-11-07); it stays emulated. const KIRO_NO_XHIGH_NATIVE_MODELS = new Set(["claude-opus-4.6", "claude-sonnet-4.6"]); +// These models used the emulated block for every effort before native effort was verified, so an +// effort outside the native ladder (`minimal`) keeps that block rather than becoming an error. +const KIRO_EMULATED_EXTRA_EFFORT_MODELS = new Set([ + "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", +]); export function kiroNativeEffortField( modelId: string, @@ -59,9 +70,17 @@ export function kiroNativeEffortField( if ((model === "gpt-5.6-luna" || model === "gpt-5.6-terra") && effort !== undefined && !KIRO_LUNA_TERRA_NATIVE_EFFORTS.has(effort)) return undefined; if (KIRO_NO_XHIGH_NATIVE_MODELS.has(model) && effort === "xhigh") return undefined; + if (KIRO_EMULATED_EXTRA_EFFORT_MODELS.has(model) && effort !== undefined && !KIRO_NATIVE_EFFORTS.includes(effort)) { + return undefined; + } return KIRO_NATIVE_EFFORT_FIELDS[model]; } +/** True when an effort outside the native ladder keeps the emulated path instead of failing. */ +export function kiroEmulatesUnsupportedEffort(modelId: string): boolean { + return KIRO_EMULATED_EXTRA_EFFORT_MODELS.has(normalizeKiroModelId(modelId)); +} + export function kiroReasoningMode(modelId: string, effort?: string): KiroReasoningMode { return kiroNativeEffortField(modelId, effort) ? "native" : "emulated"; } diff --git a/src/adapters/kiro/structured-output.ts b/src/adapters/kiro/structured-output.ts index eb68d03611b..4df8ae721ac 100644 --- a/src/adapters/kiro/structured-output.ts +++ b/src/adapters/kiro/structured-output.ts @@ -1,6 +1,6 @@ import type { AdapterEvent, OcxParsedRequest, OcxUsage } from "../../types"; import { KIRO_STRUCTURED_OUTPUT_TOOL_NAME } from "../kiro-constants"; -import { sanitizeKiroSchema } from "../kiro-tools"; +import { kiroRejectedSchemaKeys, sanitizeKiroSchema } from "../kiro-tools"; import { readKiroJson } from "./structured-output-json"; import { assertKiroStrictSchemaSupported, kiroStrictSchemaProblem, rewriteKiroWrappedRefs } from "./structured-output-schema"; @@ -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 diff --git a/tests/providers/kiro/kiro-adapter.test.ts b/tests/providers/kiro/kiro-adapter.test.ts index 33f0739fff8..06bf5537ceb 100644 --- a/tests/providers/kiro/kiro-adapter.test.ts +++ b/tests/providers/kiro/kiro-adapter.test.ts @@ -1720,13 +1720,25 @@ 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", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-6-sol", "gpt-6-luna", "claude-opus-5", "claude-opus-5.5", "claude-sonnet-5.5", "claude-opus-4.6", "claude-haiku-5.5"]) { + for (const modelId of ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-6-sol", "gpt-6-luna", "claude-opus-5", "claude-opus-5.5"]) { await expect(createKiroAdapter(provider).buildRequest({ ...parsedWith([{ role: "user", content: "solve" }], undefined, modelId), options: { reasoning: "minimal" }, })).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(); + expect(body.conversationState.currentMessage.userInputMessage.content).toContain("100"); + } + }); }); describe("kiro adapter — per-model context windows (kiro.dev/docs/models)", () => { diff --git a/tests/providers/kiro/kiro-structured-output.test.ts b/tests/providers/kiro/kiro-structured-output.test.ts index 970b4d78044..32803e87d69 100644 --- a/tests/providers/kiro/kiro-structured-output.test.ts +++ b/tests/providers/kiro/kiro-structured-output.test.ts @@ -116,11 +116,9 @@ 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. - 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. + // Sanitized the way the model's client tools are: Claude models keep every keyword. + expect(tools[0].toolSpecification.inputSchema.json).toEqual(titleSchema); + // 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 +134,39 @@ 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("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 }, }); + const claude = await payloadOf(structuredRequest({ type: "json_schema", name: "items", schema })); + 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 () => { From 7932507a4459c7d5796e0ff61754915ecf41418a Mon Sep 17 00:00:00 2001 From: JUN Date: Tue, 10 Nov 2026 19:36:01 +0900 Subject: [PATCH 09/17] test(kiro): split the catalog tests and document the effort exceptions Move the catalog budget, description limit, tool-catalog nudge and schema policy cases out of kiro-adapter.test.ts into kiro-tool-catalog.test.ts. That file had reached 2,201 lines against its 2,050-line ratchet cap; the move keeps every existing case and assertion unchanged. Strengthen the minimal-effort regression to assert both halves of the emulated block (thinking_mode and the thinking instruction), not only the budget tag. Cover the reviewer's nudge concern: a non-Claude Kiro model reached from the Anthropic Messages surface drops the Codex nudge for the same surface-scoped reason a Claude model does, while the Codex surface keeps it. Document the native effort exceptions that actually ship: the newly enabled Claude models send output_config.effort, the 4.6 pair keeps xhigh emulated, and every newly enabled Claude model except Opus 5 / 5.5 keeps minimal emulated. The canonical page, all seven translation pages and structure/providers/kiro.md now agree with src/adapters/kiro/reasoning.ts. A model is named as emulated only when it is not among the native entries above it, so the paragraph no longer covers the GPT models it just described as native. The reference also states what disabling the tool budget does: no priority sort, no omission notice, every declared tool on the wire, and the upstream ValidationException as the failure mode for an oversized catalog. (cherry picked from commit 8703f2521fa4c5ac7f226b01488e6bfad0377aa6) --- .../src/content/docs/fr/reference/adapters.md | 18 +- .../src/content/docs/ja/reference/adapters.md | 20 +- .../src/content/docs/ko/reference/adapters.md | 20 +- .../src/content/docs/reference/adapters.md | 30 ++- .../src/content/docs/ru/reference/adapters.md | 22 +- .../src/content/docs/tr/reference/adapters.md | 23 +- .../content/docs/zh-cn/reference/adapters.md | 15 +- .../content/docs/zh-tw/reference/adapters.md | 15 +- scripts/test-layout/layout.json | 2 +- structure/providers/kiro.md | 58 ++++- tests/fixtures/test-layout-expected.json | 1 + tests/providers/kiro/kiro-adapter.test.ts | 172 +------------- .../providers/kiro/kiro-tool-catalog.test.ts | 219 ++++++++++++++++++ 13 files changed, 407 insertions(+), 208 deletions(-) create mode 100644 tests/providers/kiro/kiro-tool-catalog.test.ts diff --git a/docs-site/src/content/docs/fr/reference/adapters.md b/docs-site/src/content/docs/fr/reference/adapters.md index d45e12b48a1..d849b977a47 100644 --- a/docs-site/src/content/docs/fr/reference/adapters.md +++ b/docs-site/src/content/docs/fr/reference/adapters.md @@ -142,11 +142,23 @@ 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`, +ainsi que les autres modèles 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 `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. +`gpt-5.6-sol` et les modèles GPT-6 (`gpt-6-sol`, `gpt-6-luna`, `gpt-6.1-sol`, `gpt-6.1-terra`) +conservent leurs niveaux natifs `low`, `medium`, `high`, `xhigh` et `max`. + +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`. Deux niveaux restent sur le bloc de réflexion émulé au lieu du champ natif : +`claude-opus-4.6` et `claude-sonnet-4.6` rejettent le `xhigh` natif, et tous les modèles de cette +liste sauf `claude-opus-5` et `claude-opus-5.5` considèrent `minimal` comme un niveau hors de +l’échelle native de Kiro : il conserve donc le bloc émulé au lieu de faire échouer la requête. Ces +deux modèles Opus 5 rejettent `minimal` comme une erreur de configuration, tout comme les modèles +GPT qui portent un effort natif. `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..e27ddae11e1 100644 --- a/docs-site/src/content/docs/ja/reference/adapters.md +++ b/docs-site/src/content/docs/ja/reference/adapters.md @@ -155,12 +155,22 @@ 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 系は `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-sol` と GPT-6 系(`gpt-6-sol`、`gpt-6-luna`、`gpt-6.1-sol`、`gpt-6.1-terra`)は +ネイティブの `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` で `output_config.effort` を受け付けます。次の 2 つの段階はネイティブフィールドではなく +エミュレーションの thinking ブロックを使います。`claude-opus-4.6` と `claude-sonnet-4.6` はネイティブの +`xhigh` を拒否し、`claude-opus-5` と `claude-opus-5.5` を除くこのリストの全モデルは `minimal` を +Kiro のネイティブ段階の外にある effort として扱うため、リクエストを失敗させずにエミュレーションの +ブロックを維持します。この 2 つの Opus 5 モデルは、ネイティブ effort を持つ GPT 系と同じく `minimal` を +設定エラーとして拒否します。`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..3228fe5bb70 100644 --- a/docs-site/src/content/docs/ko/reference/adapters.md +++ b/docs-site/src/content/docs/ko/reference/adapters.md @@ -188,12 +188,22 @@ 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 계열은 `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-sol`과 GPT-6 계열(`gpt-6-sol`, `gpt-6-luna`, `gpt-6.1-sol`, `gpt-6.1-terra`)은 +네이티브 `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`에 `output_config.effort`를 받아들입니다. 두 단계는 네이티브 필드 대신 +에뮬레이션 thinking 블록을 씁니다. `claude-opus-4.6`과 `claude-sonnet-4.6`은 네이티브 `xhigh`를 +거부하고, `claude-opus-5`와 `claude-opus-5.5`를 뺀 이 목록의 모든 모델은 `minimal`을 Kiro의 +네이티브 단계 밖 effort로 보아 요청을 실패시키는 대신 에뮬레이션 블록을 유지합니다. 이 두 Opus 5 +모델은 네이티브 effort를 쓰는 GPT 계열처럼 `minimal`을 설정 오류로 거부합니다. +`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 ff837de8b54..61f0743becb 100644 --- a/docs-site/src/content/docs/reference/adapters.md +++ b/docs-site/src/content/docs/reference/adapters.md @@ -582,7 +582,12 @@ Use `liveModels: false` for a static catalog; Settings can switch between the tw 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. + 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 others) from tool and structured-output schemas, except for Claude models, which accept them and keep the constraints. @@ -724,12 +729,23 @@ 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. +and the other models 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 (`gpt-6-sol`, `gpt-6-luna`, `gpt-6.1-sol`, `gpt-6.1-terra`) keep their +native `low`, `medium`, `high`, `xhigh`, and `max` behavior. + +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`. Two rungs stay on the +emulated thinking block instead of the native field: `claude-opus-4.6` and `claude-sonnet-4.6` +reject native `xhigh`, and every model in that list except `claude-opus-5` and +`claude-opus-5.5` treats `minimal` as an effort outside Kiro's native ladder, so it keeps the +emulated block rather than failing the request. Those two Opus 5 models reject `minimal` as a +configuration error, as do the GPT models that carry native effort. `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..5c8cf5f6659 100644 --- a/docs-site/src/content/docs/ru/reference/adapters.md +++ b/docs-site/src/content/docs/ru/reference/adapters.md @@ -190,12 +190,22 @@ 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 использует `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` и модели GPT-6 (`gpt-6-sol`, `gpt-6-luna`, `gpt-6.1-sol`, +`gpt-6.1-terra`) сохраняют нативные `low`, `medium`, `high`, `xhigh` и `max`. + +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`. Два уровня остаются на эмулируемом блоке thinking вместо нативного поля: +`claude-opus-4.6` и `claude-sonnet-4.6` отклоняют нативный `xhigh`, а все модели этого списка, +кроме `claude-opus-5` и `claude-opus-5.5`, считают `minimal` уровнем вне нативной шкалы Kiro и +сохраняют эмулируемый блок вместо ошибки запроса. Эти две модели Opus 5 отклоняют `minimal` как +ошибку конфигурации, как и модели GPT с нативным effort. `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..f59fdfc2e46 100644 --- a/docs-site/src/content/docs/tr/reference/adapters.md +++ b/docs-site/src/content/docs/tr/reference/adapters.md @@ -279,12 +279,23 @@ 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 `additionalModelRequestFields.reasoning.effort`, `claude-opus-5` ile aşağıda +listelenen diğer modeller 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 (`gpt-6-sol`, +`gpt-6-luna`, `gpt-6.1-sol`, `gpt-6.1-terra`) yerel `low`, `medium`, `high`, `xhigh` ve `max` +davranışını 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 kabul eder. İki seviye yerel alan yerine öykünülen düşünme bloğunu +kullanır: `claude-opus-4.6` ve `claude-sonnet-4.6` yerel `xhigh` seviyesini reddeder ve bu listedeki +`claude-opus-5` ile `claude-opus-5.5` dışındaki tüm modeller `minimal` seviyesini Kiro'nun yerel +basamağı dışında bir çaba sayar; istek başarısız olmak yerine öykünülen bloğu korur. Bu iki Opus 5 +modeli, yerel çaba kullanan GPT modelleri gibi `minimal` seviyesini yapılandırma hatası olarak +reddeder. `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..bcd95239ad0 100644 --- a/docs-site/src/content/docs/zh-cn/reference/adapters.md +++ b/docs-site/src/content/docs/zh-cn/reference/adapters.md @@ -166,12 +166,21 @@ Kiro 的 assistant 文本本身没有可靠的回合结束标记,但终止的 ### Reasoning effort -GPT-5.6 系列使用 `additionalModelRequestFields.reasoning.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 选项并不代表原生支持。 +`gpt-5.6-sol` 和 GPT-6 系列(`gpt-6-sol`、`gpt-6-luna`、`gpt-6.1-sol`、`gpt-6.1-terra`) +保留原生 `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` 接受 `output_config.effort`。有两个档位改用模拟 thinking 块而不是原生字段: +`claude-opus-4.6` 和 `claude-sonnet-4.6` 拒绝原生 `xhigh`;该列表中除 `claude-opus-5` 和 +`claude-opus-5.5` 之外的所有模型都把 `minimal` 视为 Kiro 原生档位之外的 effort,因此保留模拟块而不会让请求失败。 +这两个 Opus 5 模型把 `minimal` 当作配置错误拒绝,与使用原生 effort 的 GPT 系列相同。 +`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..bf392bf66ae 100644 --- a/docs-site/src/content/docs/zh-tw/reference/adapters.md +++ b/docs-site/src/content/docs/zh-tw/reference/adapters.md @@ -146,12 +146,21 @@ Kiro 的 assistant 文字本身沒有可靠的回合結束標記,但終止的 ### Reasoning effort -GPT-5.6 系列使用 `additionalModelRequestFields.reasoning.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 選項不代表原生支援。 +`gpt-5.6-sol` 和 GPT-6 系列(`gpt-6-sol`、`gpt-6-luna`、`gpt-6.1-sol`、`gpt-6.1-terra`) +保留原生 `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` 接受 `output_config.effort`。有兩個檔位改用模擬 thinking 區塊而非原生欄位: +`claude-opus-4.6` 和 `claude-sonnet-4.6` 拒絕原生 `xhigh`;該清單中除 `claude-opus-5` 和 +`claude-opus-5.5` 以外的所有模型都把 `minimal` 視為 Kiro 原生檔位之外的 effort,因此保留模擬區塊而不會讓請求失敗。 +這兩個 Opus 5 模型把 `minimal` 當成設定錯誤拒絕,與使用原生 effort 的 GPT 系列相同。 +`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 27552a768c6..f68d4b531b5 100644 --- a/scripts/test-layout/layout.json +++ b/scripts/test-layout/layout.json @@ -1100,7 +1100,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/structure/providers/kiro.md b/structure/providers/kiro.md index 547ee8458e4..f427c4c62bf 100644 --- a/structure/providers/kiro.md +++ b/structure/providers/kiro.md @@ -133,6 +133,46 @@ 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, not per transport. `kiroRejectedSchemaKeys(modelId)` +returns an empty set for `claude-*` models — the 2026-11-07 probe accepted all 28 rejected +keywords there, one per request — and `KIRO_REJECTED_SCHEMA_KEYS` for every other family. 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: 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 @@ -290,10 +330,20 @@ 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's native effort ladder has no `minimal` +rung; the seven Claude models that gained native effort in #8451 used the emulated block for +every effort before, so `KIRO_EMULATED_EXTRA_EFFORT_MODELS` keeps that block for an effort +outside the ladder instead of turning it into a request error. `claude-opus-5` and +`claude-opus-5.5` are deliberately absent from that set: they had native effort before the +change, and `minimal` stays a configuration error for them, as it is for the GPT models with +native effort. 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 c069bb9bd9c..49817b1afa3 100644 --- a/tests/fixtures/test-layout-expected.json +++ b/tests/fixtures/test-layout-expected.json @@ -990,6 +990,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 06bf5537ceb..29f0badfb1a 100644 --- a/tests/providers/kiro/kiro-adapter.test.ts +++ b/tests/providers/kiro/kiro-adapter.test.ts @@ -14,7 +14,7 @@ import { KIRO_TOOL_RESULT_CARRIER_MESSAGE, } from "../../../src/adapters/kiro-constants"; import { EMPTY_EXEC_OUTPUT_MESSAGE, FAILED_EXEC_OUTPUT_MESSAGE } from "../../../src/adapters/exec-tool-result-normalize"; -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 { MAX_KIRO_TOOL_CATALOG_BYTES, MAX_KIRO_TOOL_COUNT } from "../../../src/adapters/kiro-tools"; import { applyProviderConfigHints, buildCatalogEntries } from "../../../src/codex/catalog"; import { getValidAccessTokenSnapshot } from "../../../src/oauth"; import { saveCredential } from "../../../src/oauth/store"; @@ -1736,7 +1736,12 @@ describe("kiro adapter — native and emulated reasoning effort", () => { })).body); expect(body.additionalModelRequestFields).toBeUndefined(); - expect(body.conversationState.currentMessage.userInputMessage.content).toContain("100"); + // 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(""); } }); }); @@ -2036,166 +2041,3 @@ describe("kiro code-mode exec survives the catalog budget", () => { expect(serializedBytes).toBeLessThanOrEqual(MAX_KIRO_TOOL_CATALOG_BYTES); }); }); - -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): Promise { - const parsed = parsedWith([{ role: "user", content: "hi" }], [bashTool]); - 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"); - }); -}); - -describe("kiro tool schemas by model family", () => { - // Live probe on 2026-11-07: Claude models on Kiro accept every JSON Schema keyword the - // sanitizer used to strip, so the constraints reach the model. Other families keep the strip. - 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"], - }; - - 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("a Claude model receives the schema with every constraint", async () => { - for (const modelId of ["claude-opus-5.5", "claude-sonnet-5.5", "claude-haiku-5.5", "claude-opus-4.5"]) { - expect(await emittedSchema(modelId)).toEqual(constrained); - } - }); - - 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 } } }] }, - }], "claude-opus-5.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 }); - }); -}); 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..3cc5430fadb --- /dev/null +++ b/tests/providers/kiro/kiro-tool-catalog.test.ts @@ -0,0 +1,219 @@ +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", () => { + // Live probe on 2026-11-07: Claude models on Kiro accept every JSON Schema keyword the + // sanitizer used to strip, so the constraints reach the model. Other families keep the strip. + 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"], + }; + + 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("a Claude model receives the schema with every constraint", async () => { + for (const modelId of ["claude-opus-5.5", "claude-sonnet-5.5", "claude-haiku-5.5", "claude-opus-4.5"]) { + expect(await emittedSchema(modelId)).toEqual(constrained); + } + }); + + 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 } } }] }, + }], "claude-opus-5.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 }); + }); +}); From a6e2a5defb60f7d598d55bf07d605dbfe217b40e Mon Sep 17 00:00:00 2001 From: JUN Date: Wed, 11 Nov 2026 01:07:35 +0900 Subject: [PATCH 10/17] fix(kiro): scope schema exceptions to reported evidence (cherry picked from commit 6a939b5fb4bc66cfd8363373254d482c586a696e) --- .../src/content/docs/fr/reference/adapters.md | 23 ++++--- .../src/content/docs/ja/reference/adapters.md | 37 +++++++++--- .../src/content/docs/ko/reference/adapters.md | 36 ++++++++--- .../src/content/docs/reference/adapters.md | 28 +++++---- .../src/content/docs/ru/reference/adapters.md | 40 ++++++++++--- .../src/content/docs/tr/reference/adapters.md | 39 +++++++++--- .../content/docs/zh-cn/reference/adapters.md | 31 ++++++++-- .../content/docs/zh-tw/reference/adapters.md | 31 ++++++++-- src/adapters/kiro-tools.ts | 34 +++++++++-- src/adapters/kiro/payload.ts | 2 +- src/adapters/kiro/reasoning.ts | 22 +++++-- src/adapters/kiro/structured-output.ts | 14 +++-- structure/providers/kiro.md | 59 +++++++++++++----- .../kiro/kiro-structured-output.test.ts | 44 +++++++++++++- .../providers/kiro/kiro-tool-catalog.test.ts | 60 +++++++++++++++++-- 15 files changed, 402 insertions(+), 98 deletions(-) diff --git a/docs-site/src/content/docs/fr/reference/adapters.md b/docs-site/src/content/docs/fr/reference/adapters.md index d849b977a47..f11a8dddfbf 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. @@ -151,14 +153,19 @@ conservent leurs niveaux natifs `low`, `medium`, `high`, `xhigh` et `max`. 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`. Deux niveaux restent sur le bloc de réflexion émulé au lieu du champ natif : -`claude-opus-4.6` et `claude-sonnet-4.6` rejettent le `xhigh` natif, et tous les modèles de cette -liste sauf `claude-opus-5` et `claude-opus-5.5` considèrent `minimal` comme un niveau hors de -l’échelle native de Kiro : il conserve donc le bloc émulé au lieu de faire échouer la requête. Ces -deux modèles Opus 5 rejettent `minimal` comme une erreur de configuration, tout comme les modèles -GPT qui portent un effort natif. `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. +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` +nommant le modèle et le niveau. `ultra` n’atteint jamais Kiro : les chemins Responses et Messages +le réduisent tous deux à `max` avant que l’adaptateur ne construise la requête. +`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 e27ddae11e1..9db76483b97 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 を示す信頼できる区別がありません。ただし終端の @@ -164,13 +183,17 @@ GPT-5.6 系は `additionalModelRequestFields.reasoning.effort`、`claude-opus-5` 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` で `output_config.effort` を受け付けます。次の 2 つの段階はネイティブフィールドではなく -エミュレーションの thinking ブロックを使います。`claude-opus-4.6` と `claude-sonnet-4.6` はネイティブの -`xhigh` を拒否し、`claude-opus-5` と `claude-opus-5.5` を除くこのリストの全モデルは `minimal` を -Kiro のネイティブ段階の外にある effort として扱うため、リクエストを失敗させずにエミュレーションの -ブロックを維持します。この 2 つの Opus 5 モデルは、ネイティブ effort を持つ GPT 系と同じく `minimal` を -設定エラーとして拒否します。`claude-haiku-4.5`、`claude-opus-4.5`、`claude-sonnet-4.5` と、 -上記にない Kiro モデルはすべての effort でエミュレーションを使います。effort の選択肢だけではネイティブ対応を意味しません。 +`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` を +モデルと effort を示す 400 `invalid_request_error` で拒否します。`ultra` は Kiro に届きません。 +Responses と Messages のどちらの経路も、アダプターがリクエストを組み立てる前に `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/ko/reference/adapters.md b/docs-site/src/content/docs/ko/reference/adapters.md index 3228fe5bb70..906c27920c9 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의 어시스턴트 텍스트에는 그 자체로 턴 종료를 알리는 신뢰할 만한 구분이 없습니다. 다만 종단 @@ -197,13 +215,17 @@ GPT-5.6 계열은 `additionalModelRequestFields.reasoning.effort`를, `claude-op 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`에 `output_config.effort`를 받아들입니다. 두 단계는 네이티브 필드 대신 -에뮬레이션 thinking 블록을 씁니다. `claude-opus-4.6`과 `claude-sonnet-4.6`은 네이티브 `xhigh`를 -거부하고, `claude-opus-5`와 `claude-opus-5.5`를 뺀 이 목록의 모든 모델은 `minimal`을 Kiro의 -네이티브 단계 밖 effort로 보아 요청을 실패시키는 대신 에뮬레이션 블록을 유지합니다. 이 두 Opus 5 -모델은 네이티브 effort를 쓰는 GPT 계열처럼 `minimal`을 설정 오류로 거부합니다. -`claude-haiku-4.5`, `claude-opus-4.5`, `claude-sonnet-4.5`와 위에 없는 Kiro 모델은 모든 effort에서 -에뮬레이션을 씁니다. 조절 항목이 있다고 네이티브 지원을 뜻하지는 않습니다. +`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`을 모델과 effort를 +밝히는 400 `invalid_request_error`로 거부합니다. `ultra`는 Kiro에 도달하지 않습니다. Responses와 +Messages 경로 모두 어댑터가 요청을 만들기 전에 `max`로 낮춥니다. +`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 61f0743becb..920083aafff 100644 --- a/docs-site/src/content/docs/reference/adapters.md +++ b/docs-site/src/content/docs/reference/adapters.md @@ -589,8 +589,12 @@ Use `liveModels: false` for a static catalog; Settings can switch between the tw 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 others) from tool and structured-output schemas, except for Claude - models, which accept them and keep the constraints. + `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 the PR does not report them. 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 @@ -738,14 +742,18 @@ native `low`, `medium`, `high`, `xhigh`, and `max` behavior. 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`. Two rungs stay on the -emulated thinking block instead of the native field: `claude-opus-4.6` and `claude-sonnet-4.6` -reject native `xhigh`, and every model in that list except `claude-opus-5` and -`claude-opus-5.5` treats `minimal` as an effort outside Kiro's native ladder, so it keeps the -emulated block rather than failing the request. Those two Opus 5 models reject `minimal` as a -configuration error, as do the GPT models that carry native effort. `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. +`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` naming the model and the effort. `ultra` never +reaches Kiro: the Responses and Messages paths both reduce it to `max` before the adapter builds +a request. `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 5c8cf5f6659..45be67e2648 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 сам по себе не даёт надёжного признака конца хода. Однако завершающий @@ -199,13 +220,18 @@ incomplete. `TOOL_USE` без фактического вызова инстру 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`. Два уровня остаются на эмулируемом блоке thinking вместо нативного поля: -`claude-opus-4.6` и `claude-sonnet-4.6` отклоняют нативный `xhigh`, а все модели этого списка, -кроме `claude-opus-5` и `claude-opus-5.5`, считают `minimal` уровнем вне нативной шкалы Kiro и -сохраняют эмулируемый блок вместо ошибки запроса. Эти две модели Opus 5 отклоняют `minimal` как -ошибку конфигурации, как и модели GPT с нативным effort. `claude-haiku-4.5`, `claude-opus-4.5`, -`claude-sonnet-4.5` и любая не перечисленная выше модель Kiro используют эмуляцию на каждом -уровне; наличие настройки effort не доказывает нативную поддержку. +и `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` с указанием модели и уровня. `ultra` до Kiro не доходит: и путь +Responses, и путь Messages сводят его к `max` до того, как адаптер построит запрос. +`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 f59fdfc2e46..74607ac2aef 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ı o 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 @@ -289,13 +309,18 @@ davranışını 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 kabul eder. İki seviye yerel alan yerine öykünülen düşünme bloğunu -kullanır: `claude-opus-4.6` ve `claude-sonnet-4.6` yerel `xhigh` seviyesini reddeder ve bu listedeki -`claude-opus-5` ile `claude-opus-5.5` dışındaki tüm modeller `minimal` seviyesini Kiro'nun yerel -basamağı dışında bir çaba sayar; istek başarısız olmak yerine öykünülen bloğu korur. Bu iki Opus 5 -modeli, yerel çaba kullanan GPT modelleri gibi `minimal` seviyesini yapılandırma hatası olarak -reddeder. `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. +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 modeli ve seviyeyi belirten 400 `invalid_request_error` ile reddeder. +`ultra` Kiro'ya hiç ulaşmaz: hem Responses hem Messages yolu, adaptör isteği kurmadan önce onu +`max` seviyesine indirir. `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 bcd95239ad0..c611ab9da01 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`。 @@ -175,12 +190,16 @@ GPT-5.6 系列使用 `additionalModelRequestFields.reasoning.effort`,`claude-o 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` 接受 `output_config.effort`。有两个档位改用模拟 thinking 块而不是原生字段: -`claude-opus-4.6` 和 `claude-sonnet-4.6` 拒绝原生 `xhigh`;该列表中除 `claude-opus-5` 和 -`claude-opus-5.5` 之外的所有模型都把 `minimal` 视为 Kiro 原生档位之外的 effort,因此保留模拟块而不会让请求失败。 -这两个 Opus 5 模型把 `minimal` 当作配置错误拒绝,与使用原生 effort 的 GPT 系列相同。 -`claude-haiku-4.5`、`claude-opus-4.5`、`claude-sonnet-4.5` 以及上面未列出的 Kiro 模型在每个 effort 上 -都使用模拟推理;提供 effort 选项并不代表原生支持。 +`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`,并在错误中标明模型和 +effort。`ultra` 永远不会到达 Kiro:Responses 和 Messages 路径都会在适配器构建请求前把它降为 +`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 bf392bf66ae..733fbb70e68 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 @@ -155,12 +170,16 @@ GPT-5.6 系列使用 `additionalModelRequestFields.reasoning.effort`,`claude-o 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` 接受 `output_config.effort`。有兩個檔位改用模擬 thinking 區塊而非原生欄位: -`claude-opus-4.6` 和 `claude-sonnet-4.6` 拒絕原生 `xhigh`;該清單中除 `claude-opus-5` 和 -`claude-opus-5.5` 以外的所有模型都把 `minimal` 視為 Kiro 原生檔位之外的 effort,因此保留模擬區塊而不會讓請求失敗。 -這兩個 Opus 5 模型把 `minimal` 當成設定錯誤拒絕,與使用原生 effort 的 GPT 系列相同。 -`claude-haiku-4.5`、`claude-opus-4.5`、`claude-sonnet-4.5` 以及上面未列出的 Kiro 模型在每個 effort 上 -都使用模擬推理;提供 effort 選項不代表原生支援。 +`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`,並在錯誤中標明模型和 +effort。`ultra` 永遠不會到達 Kiro:Responses 和 Messages 路徑都會在適配器建置請求前把它降為 +`max`。`claude-haiku-4.5`、`claude-opus-4.5`、`claude-sonnet-4.5` 以及上面未列出的 Kiro +模型在每個 effort 上都使用模擬推理;提供 effort 選項不代表原生支援。 ## `cursor` diff --git a/src/adapters/kiro-tools.ts b/src/adapters/kiro-tools.ts index 9ecc231d8b6..f1bc5412db0 100644 --- a/src/adapters/kiro-tools.ts +++ b/src/adapters/kiro-tools.ts @@ -21,8 +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. -// Claude models accept every one of these keys (live probe on 2026-11-07, one key per request -// plus a full Claude Code catalog), so their schemas keep the constraints the model needs. +// 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", @@ -73,9 +73,35 @@ const SCHEMA_LITERAL_KEYS = new Set(["const", "enum", "default", "examples"]); const NO_SCHEMA_KEYS: ReadonlySet = new Set(); -/** Schema keywords Kiro rejects for `modelId`: none for Claude models, the measured set otherwise. */ +/** + * 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 accepted on opus-4.5, with if/then/else grouped +// in its count of 28. 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 normalizeKiroModelId(modelId).startsWith("claude-") ? NO_SCHEMA_KEYS : KIRO_REJECTED_SCHEMA_KEYS; + return KIRO_SCHEMA_KEY_EXCEPTIONS.get(normalizeKiroModelId(modelId)) ?? KIRO_REJECTED_SCHEMA_KEYS; } function sanitizeSchemaMap(value: unknown, rejected: ReadonlySet = KIRO_REJECTED_SCHEMA_KEYS): unknown { diff --git a/src/adapters/kiro/payload.ts b/src/adapters/kiro/payload.ts index 8f8de5dbd0d..382071dac78 100644 --- a/src/adapters/kiro/payload.ts +++ b/src/adapters/kiro/payload.ts @@ -488,7 +488,7 @@ export function buildKiroPayload( const effort = parsed.options.reasoning; const effortField = kiroNativeEffortField(parsed.modelId); if (effortField && effort && effort !== "none") { - if (!KIRO_NATIVE_EFFORTS.includes(effort) && !kiroEmulatesUnsupportedEffort(parsed.modelId)) { + 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 094473a5d75..ee5e1131a4e 100644 --- a/src/adapters/kiro/reasoning.ts +++ b/src/adapters/kiro/reasoning.ts @@ -31,7 +31,9 @@ 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. @@ -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/structure/providers/kiro.md b/structure/providers/kiro.md index f427c4c62bf..4dba25ce890 100644 --- a/structure/providers/kiro.md +++ b/structure/providers/kiro.md @@ -157,13 +157,31 @@ intact and the upstream `ValidationException` becomes the failure mode instead o request. The default remains bounded because that is the shape a fresh Kiro account is measured to accept. -Schema sanitization is chosen per model, not per transport. `kiroRejectedSchemaKeys(modelId)` -returns an empty set for `claude-*` models — the 2026-11-07 probe accepted all 28 rejected -keywords there, one per request — and `KIRO_REJECTED_SCHEMA_KEYS` for every other family. Both -the client-tool path and the structured-output tool (`kiroStructuredOutputSpec` in +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 the rejected keywords (counting `if/then/else` +as one item in its total of 28), so its exception is the empty set. 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-4.8`, `claude-opus-4.7`, +`claude-opus-4.6`, `claude-sonnet-5`, `claude-sonnet-4.6`, `claude-haiku-5.5`, +`claude-haiku-4.5`, `claude-sonnet-4.0`) 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: it is a wire requirement, not a constraint strip. +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` @@ -192,7 +210,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 @@ -337,13 +356,25 @@ individual rung on the emulated path. Luna and terra send only `low`, `medium`, `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's native effort ladder has no `minimal` -rung; the seven Claude models that gained native effort in #8451 used the emulated block for -every effort before, so `KIRO_EMULATED_EXTRA_EFFORT_MODELS` keeps that block for an effort -outside the ladder instead of turning it into a request error. `claude-opus-5` and -`claude-opus-5.5` are deliberately absent from that set: they had native effort before the -change, and `minimal` stays a configuration error for them, as it is for the GPT models with -native effort. A future shared effort rung does not widen any of these allowlists. +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. `minimal` is the only +outside-ladder value that reaches a Kiro request: the Responses parser folds `ultra` to `max`, as +does the Messages translator, 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. + +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` naming the model and the effort. 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/providers/kiro/kiro-structured-output.test.ts b/tests/providers/kiro/kiro-structured-output.test.ts index 32803e87d69..474d0f3d438 100644 --- a/tests/providers/kiro/kiro-structured-output.test.ts +++ b/tests/providers/kiro/kiro-structured-output.test.ts @@ -116,8 +116,10 @@ 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 the model's client tools are: Claude models keep every keyword. - expect(tools[0].toolSpecification.inputSchema.json).toEqual(titleSchema); + // 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"], + }); // 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)); @@ -146,6 +148,36 @@ describe("Kiro structured-output payload", () => { }); }); + 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" } }], @@ -157,7 +189,13 @@ describe("Kiro structured-output payload", () => { required: ["value"], $defs: { item }, }); - const claude = await payloadOf(structuredRequest({ type: "json_schema", name: "items", schema })); + // 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({ diff --git a/tests/providers/kiro/kiro-tool-catalog.test.ts b/tests/providers/kiro/kiro-tool-catalog.test.ts index 3cc5430fadb..b726d6ff6e2 100644 --- a/tests/providers/kiro/kiro-tool-catalog.test.ts +++ b/tests/providers/kiro/kiro-tool-catalog.test.ts @@ -168,8 +168,8 @@ describe("kiro tool-catalog nudge and the Anthropic Messages surface", () => { }); describe("kiro tool schemas by model family", () => { - // Live probe on 2026-11-07: Claude models on Kiro accept every JSON Schema keyword the - // sanitizer used to strip, so the constraints reach the model. Other families keep the strip. + // 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", @@ -181,6 +181,25 @@ describe("kiro tool schemas by model family", () => { }, 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( @@ -189,9 +208,22 @@ describe("kiro tool schemas by model family", () => { return JSON.parse(body).conversationState.currentMessage.userInputMessage.userInputMessageContext.tools[0].toolSpecification.inputSchema.json; } - test("a Claude model receives the schema with every constraint", async () => { - for (const modelId of ["claude-opus-5.5", "claude-sonnet-5.5", "claude-haiku-5.5", "claude-opus-4.5"]) { - expect(await emittedSchema(modelId)).toEqual(constrained); + 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); } }); @@ -209,7 +241,9 @@ describe("kiro tool schemas by model family", () => { 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 } } }] }, - }], "claude-opus-5.5"), + // 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"); @@ -217,3 +251,17 @@ describe("kiro tool schemas by model family", () => { 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 reachable outside-ladder value is `minimal`; `ultra` is folded to `max` upstream. 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"`); + } + }); +}); From 53ab9ba4fad27827df6c22fc967dd375d50e732d Mon Sep 17 00:00:00 2001 From: JUN Date: Thu, 12 Nov 2026 16:47:23 +0900 Subject: [PATCH 11/17] docs(kiro): correct the GPT-6 effort field and the unprobed Claude roster The adapters reference assigned the GPT-6 models to `output_config.effort` by grouping them with the Claude ids behind a "the other models listed below" clause. `src/adapters/kiro/reasoning.ts` maps `gpt-6-sol`, `gpt-6-luna`, `gpt-6.1-sol` and `gpt-6.1-terra` to `reasoning.effort`, the same native field as the GPT-5.6 family; only the nine probed Claude ids use `output_config.effort`. Each locale now names the GPT-6 family in the `reasoning.effort` sentence and keeps `output_config.effort` for the Claude list. `structure/providers/kiro.md` listed the unprobed Claude ids for schema keyword sanitization but omitted `claude-opus-5` and `claude-sonnet-4.5`, both present in `KIRO_MODELS` and absent from `KIRO_SCHEMA_KEY_EXCEPTIONS`. The roster now names all ten unprobed ids. Docs only: no source, test, or behavior change. (cherry picked from commit 9f6a2e69f4327e92cf458619e9609a9d692a3ac3) --- .../src/content/docs/fr/reference/adapters.md | 15 ++++++++------- .../src/content/docs/ja/reference/adapters.md | 9 +++++---- .../src/content/docs/ko/reference/adapters.md | 9 +++++---- .../src/content/docs/reference/adapters.md | 9 +++++---- .../src/content/docs/ru/reference/adapters.md | 9 +++++---- .../src/content/docs/tr/reference/adapters.md | 10 +++++----- .../content/docs/zh-cn/reference/adapters.md | 7 ++++--- .../content/docs/zh-tw/reference/adapters.md | 7 ++++--- structure/providers/kiro.md | 18 ++++++++++-------- 9 files changed, 51 insertions(+), 42 deletions(-) diff --git a/docs-site/src/content/docs/fr/reference/adapters.md b/docs-site/src/content/docs/fr/reference/adapters.md index f11a8dddfbf..d4609195d62 100644 --- a/docs-site/src/content/docs/fr/reference/adapters.md +++ b/docs-site/src/content/docs/fr/reference/adapters.md @@ -143,13 +143,14 @@ 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` -ainsi que les autres modèles 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 (`gpt-6-sol`, `gpt-6-luna`, `gpt-6.1-sol`, `gpt-6.1-terra`) -conservent leurs niveaux natifs `low`, `medium`, `high`, `xhigh` et `max`. +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` diff --git a/docs-site/src/content/docs/ja/reference/adapters.md b/docs-site/src/content/docs/ja/reference/adapters.md index 9db76483b97..5bded21839c 100644 --- a/docs-site/src/content/docs/ja/reference/adapters.md +++ b/docs-site/src/content/docs/ja/reference/adapters.md @@ -174,12 +174,13 @@ filtered incomplete になります。実際のツール呼び出しを伴わな ### Reasoning effort -GPT-5.6 系は `additionalModelRequestFields.reasoning.effort`、`claude-opus-5` と -以下に挙げるその他のモデルは `additionalModelRequestFields.output_config.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 系(`gpt-6-sol`、`gpt-6-luna`、`gpt-6.1-sol`、`gpt-6.1-terra`)は -ネイティブの `low`、`medium`、`high`、`xhigh`、`max` を維持します。 +`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`、 diff --git a/docs-site/src/content/docs/ko/reference/adapters.md b/docs-site/src/content/docs/ko/reference/adapters.md index 906c27920c9..abd6ca64c9a 100644 --- a/docs-site/src/content/docs/ko/reference/adapters.md +++ b/docs-site/src/content/docs/ko/reference/adapters.md @@ -206,12 +206,13 @@ commentary로 유지하고 비공개 완료 툴을 한 번 검증합니다. ### Reasoning effort -GPT-5.6 계열은 `additionalModelRequestFields.reasoning.effort`를, `claude-opus-5`와 -아래에 나열한 다른 모델은 `additionalModelRequestFields.output_config.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 계열(`gpt-6-sol`, `gpt-6-luna`, `gpt-6.1-sol`, `gpt-6.1-terra`)은 -네이티브 `low`, `medium`, `high`, `xhigh`, `max`를 그대로 사용합니다. +`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`, diff --git a/docs-site/src/content/docs/reference/adapters.md b/docs-site/src/content/docs/reference/adapters.md index 920083aafff..2d18bd3fc74 100644 --- a/docs-site/src/content/docs/reference/adapters.md +++ b/docs-site/src/content/docs/reference/adapters.md @@ -732,13 +732,14 @@ 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` -and the other models listed below use `additionalModelRequestFields.output_config.effort`. +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 (`gpt-6-sol`, `gpt-6-luna`, `gpt-6.1-sol`, `gpt-6.1-terra`) keep their -native `low`, `medium`, `high`, `xhigh`, and `max` behavior. +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`, diff --git a/docs-site/src/content/docs/ru/reference/adapters.md b/docs-site/src/content/docs/ru/reference/adapters.md index 45be67e2648..ab5d7c58734 100644 --- a/docs-site/src/content/docs/ru/reference/adapters.md +++ b/docs-site/src/content/docs/ru/reference/adapters.md @@ -211,12 +211,13 @@ incomplete. `TOOL_USE` без фактического вызова инстру ### Reasoning effort -Семейство GPT-5.6 использует `additionalModelRequestFields.reasoning.effort`, а `claude-opus-5` -и остальные перечисленные ниже модели — `additionalModelRequestFields.output_config.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 (`gpt-6-sol`, `gpt-6-luna`, `gpt-6.1-sol`, -`gpt-6.1-terra`) сохраняют нативные `low`, `medium`, `high`, `xhigh` и `max`. +уровень не проверен. `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` diff --git a/docs-site/src/content/docs/tr/reference/adapters.md b/docs-site/src/content/docs/tr/reference/adapters.md index 74607ac2aef..2b1b1844821 100644 --- a/docs-site/src/content/docs/tr/reference/adapters.md +++ b/docs-site/src/content/docs/tr/reference/adapters.md @@ -299,13 +299,13 @@ 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` ile aşağıda -listelenen diğer modeller ise `additionalModelRequestFields.output_config.effort` alanını kullanır. +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 (`gpt-6-sol`, -`gpt-6-luna`, `gpt-6.1-sol`, `gpt-6.1-terra`) yerel `low`, `medium`, `high`, `xhigh` ve `max` -davranışını sürdürür. +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` 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 c611ab9da01..eb799512b71 100644 --- a/docs-site/src/content/docs/zh-cn/reference/adapters.md +++ b/docs-site/src/content/docs/zh-cn/reference/adapters.md @@ -181,12 +181,13 @@ 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` 和 GPT-6 系列(`gpt-6-sol`、`gpt-6-luna`、`gpt-6.1-sol`、`gpt-6.1-terra`) -保留原生 `low`、`medium`、`high`、`xhigh`、`max`。 +`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` 和 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 733fbb70e68..dfa7e487aa1 100644 --- a/docs-site/src/content/docs/zh-tw/reference/adapters.md +++ b/docs-site/src/content/docs/zh-tw/reference/adapters.md @@ -161,12 +161,13 @@ 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` 和 GPT-6 系列(`gpt-6-sol`、`gpt-6-luna`、`gpt-6.1-sol`、`gpt-6.1-terra`) -保留原生 `low`、`medium`、`high`、`xhigh`、`max`。 +`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` 和 diff --git a/structure/providers/kiro.md b/structure/providers/kiro.md index 4dba25ce890..1669bebba38 100644 --- a/structure/providers/kiro.md +++ b/structure/providers/kiro.md @@ -168,14 +168,16 @@ as one item in its total of 28), so its exception is the empty set. It reports a 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-4.8`, `claude-opus-4.7`, -`claude-opus-4.6`, `claude-sonnet-5`, `claude-sonnet-4.6`, `claude-haiku-5.5`, -`claude-haiku-4.5`, `claude-sonnet-4.0`) 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. +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 From 3907e54fbe2b9e156595e105d7cfd16ef0329601 Mon Sep 17 00:00:00 2001 From: JUN Date: Thu, 12 Nov 2026 18:27:58 +0900 Subject: [PATCH 12/17] docs(kiro): state the measured rejected-key count instead of the report's 28 The comment in `src/adapters/kiro-tools.ts` and the schema-sanitization paragraph in `structure/providers/kiro.md` described the opus-4.5 probe as grouping `if/then/else` in a total of 28. That number is the report's stated total, not the measured set: `KIRO_REJECTED_SCHEMA_KEYS` holds 29 keys, and PR #8451's enumerated list is 27 whitespace-separated items that expand to exactly those 29 once `if/then/else` is read as three keys. The list covers the whole set, so the exception stays the empty set. Both places now separate the two facts: the list enumerates 27 items and names all 29 keys, and the report states that total as 28, one fewer than the keys its own list prints. Nothing claims a probe was performed that the report does not describe, and no unmeasured key is implied to be accepted. The eight docs locales already state 29 keys (four spell out the number, four write four named keys plus 25 others) and are unchanged. Docs and comment only: no source, test, or behavior change. (cherry picked from commit 5f19c53ceee84970e52f3fbb551b8b8498438fdb) Carried from PR #8451 onto dev 95bb715bdb. The contributor's commits keep their authorship: two are the contributor's, and the six that follow (four substantive, two "Merge dev into fix/kiro-adapter" merges) are the maintainer's. Rebased clean onto current dev with no conflict in the six substantive commits; the only files dev had moved since the PR's merge base are scripts/test-layout/layout.json, tests/fixtures/test-layout-expected.json and src/server/responses/request-prepare.ts, and both sides of each survive. Co-authored-by: svc-aibot-363 <271381517+svc-aibot-363@users.noreply.github.com> --- src/adapters/kiro-tools.ts | 7 +++++-- structure/providers/kiro.md | 11 +++++++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/adapters/kiro-tools.ts b/src/adapters/kiro-tools.ts index f1bc5412db0..9beaec58bd6 100644 --- a/src/adapters/kiro-tools.ts +++ b/src/adapters/kiro-tools.ts @@ -84,8 +84,11 @@ function rejectAllExcept(accepted: readonly string[]): ReadonlySet { return new Set([...KIRO_REJECTED_SCHEMA_KEYS].filter(key => !keep.has(key))); } -// PR #8451 reports the rejected keys accepted on opus-4.5, with if/then/else grouped -// in its count of 28. Its two 5.5 probes explicitly name only pattern and maximum. +// 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], diff --git a/structure/providers/kiro.md b/structure/providers/kiro.md index 1669bebba38..503ba1befcf 100644 --- a/structure/providers/kiro.md +++ b/structure/providers/kiro.md @@ -163,10 +163,13 @@ Schema sanitization is chosen per model id and per keyword, not per family or tr 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 the rejected keywords (counting `if/then/else` -as one item in its total of 28), so its exception is the empty set. 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. +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`, From b13b70929befb22e8cf7f689b236c1d14681b080 Mon Sep 17 00:00:00 2001 From: JUN Date: Fri, 13 Nov 2026 02:54:11 +0900 Subject: [PATCH 13/17] docs(kiro): an ultra pin does reach the adapter, and seven Claude ids now reject it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The comments in `src/adapters/kiro/reasoning.ts`, `structure/providers/kiro.md` and the eight locale adapter pages claimed `ultra` never reaches the Kiro adapter. That is only true for a caller's own request. `applyPinnedEffort` (`src/server/effort-policy.ts`) runs after the body is parsed and accepts `ultra` because `isDeclaredReasoningEffort` includes the whole Codex ladder, so `pinnedReasoningEffort`, `modelPinnedReasoningEfforts` and `modelPinnedEfforts` do deliver it to `buildKiroPayload`. What happens there splits by model, and the docs now say which half each model is on: a model in `KIRO_NATIVE_EFFORT_FIELDS` throws, and the request becomes HTTP 400 `invalid_request_error` naming the model and the effort before anything is sent; a model outside it keeps the emulated path, where `kiroThinkingBudget` has no `ultra` entry, so the pin is dropped and no reasoning signal goes out. Measured through the production Responses handler against base 95bb715bdb and this head: the GPT models and Opus 5 / Opus 5.5 already rejected a pinned `ultra` on the base, so nothing changed for them. The seven Claude ids this branch made native — Opus 4.8/4.7/4.6, Sonnet 5.5/5/4.6, Haiku 5.5 — changed from silently dropping the pin to returning 400. That upgrade effect is now stated in the English page and all seven translations, and in the structure contract. The attribution was wrong in one more place: it is not the Messages translator that folds `ultra`. That translator copies `output_config.effort` through verbatim and hands the translated body to the shared Responses parser, which is what folds it for both surfaces. The code comment and the structure doc now say that. The schema-keyword exception reads "no probe sent them to those models" in English, matching what the translations already said, instead of the narrower "the PR does not report them". `tests/providers/kiro/kiro-pinned-effort-ultra.test.ts` drives the production normalization and adapter dispatch: a pin of `ultra` against every native-effort model, a caller `ultra` against the same model to show the fold still reaches the wire as `max`, an emulated model to pin down the silent-drop half, the Messages surface returning an Anthropic 400, and a caller `ultra` on Messages reaching the wire as `max`. Mapping pinned `ultra` to `max` in `applyPinnedEffort` fails four of the five. Co-authored-by: svc-aibot-363 --- .../src/content/docs/fr/reference/adapters.md | 15 +- .../src/content/docs/ja/reference/adapters.md | 13 +- .../src/content/docs/ko/reference/adapters.md | 13 +- .../src/content/docs/reference/adapters.md | 19 +- .../src/content/docs/ru/reference/adapters.md | 15 +- .../src/content/docs/tr/reference/adapters.md | 17 +- .../content/docs/zh-cn/reference/adapters.md | 12 +- .../content/docs/zh-tw/reference/adapters.md | 12 +- src/adapters/kiro/reasoning.ts | 24 ++- structure/providers/kiro.md | 30 +++- .../kiro/kiro-pinned-effort-ultra.test.ts | 167 ++++++++++++++++++ 11 files changed, 308 insertions(+), 29 deletions(-) create mode 100644 tests/providers/kiro/kiro-pinned-effort-ultra.test.ts diff --git a/docs-site/src/content/docs/fr/reference/adapters.md b/docs-site/src/content/docs/fr/reference/adapters.md index d4609195d62..98383f7e1d9 100644 --- a/docs-site/src/content/docs/fr/reference/adapters.md +++ b/docs-site/src/content/docs/fr/reference/adapters.md @@ -162,8 +162,19 @@ Kiro n’a pas de niveau `minimal`. Sur les neuf modèles ci-dessus sauf `claude `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` -nommant le modèle et le niveau. `ultra` n’atteint jamais Kiro : les chemins Responses et Messages -le réduisent tous deux à `max` avant que l’adaptateur ne construise la requête. +nommant le modèle et le niveau. + +`ultra` demande une distinction. Le `ultra` envoyé par l’appelant est réduit à `max` avant que +l’adaptateur ne construise la requête, sur les chemins Responses et Messages. Un pin configuré, non : +`pinnedReasoningEffort`, `modelPinnedReasoningEfforts` et `modelPinnedEfforts` acceptent `ultra` et +s’appliquent après l’analyse de la requête, donc l’adaptateur le reçoit. Pour chaque modèle à effort +natif Kiro — les identifiants Claude ci-dessus et les modèles GPT — ce pin fait échouer la requête +par une erreur 400 `invalid_request_error` nommant le modèle et le niveau, car `ultra` n’est pas un +palier que Kiro accepte. Pour un modèle qui utilise +l’émulation, le pin est abandonné et la requête part sans aucun signal de raisonnement. Si vous avez +fait passer l’un de ces identifiants Claude en effort natif, un pin `ultra` autrefois ignoré échoue +désormais : épinglez `max`. + `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. diff --git a/docs-site/src/content/docs/ja/reference/adapters.md b/docs-site/src/content/docs/ja/reference/adapters.md index 5bded21839c..61afc2d572a 100644 --- a/docs-site/src/content/docs/ja/reference/adapters.md +++ b/docs-site/src/content/docs/ja/reference/adapters.md @@ -191,8 +191,17 @@ Kiro は `claude-opus-5`、`claude-opus-5.5`、`claude-opus-4.8`、`claude-opus- Kiro には `minimal` の段階がありません。`claude-opus-5` と `claude-opus-5.5` を除く上記の 9 モデルは、 ネイティブ effort を有効にする前から動作していたリクエストを失敗させないよう、`minimal` でエミュレーションの ブロックを維持します。この 2 つの Opus 5 モデルとネイティブ effort を持つ GPT 系は、`minimal` を -モデルと effort を示す 400 `invalid_request_error` で拒否します。`ultra` は Kiro に届きません。 -Responses と Messages のどちらの経路も、アダプターがリクエストを組み立てる前に `max` へ下げます。 +モデルと effort を示す 400 `invalid_request_error` で拒否します。 + +`ultra` には区別が必要です。呼び出し元が送った `ultra` は、Responses と Messages のどちらの経路でも +アダプターがリクエストを組み立てる前に `max` へ下げます。設定した pin は違います。 +`pinnedReasoningEffort`、`modelPinnedReasoningEfforts`、`modelPinnedEfforts` は `ultra` を受け付け、 +リクエストを解析した後に適用するため、アダプターまで届きます。ネイティブ Kiro effort を持つすべての +モデル、つまり上記の Claude id と GPT モデルでは、この pin が Kiro の受け付けない段階であるため、 +モデルと effort を示す 400 `invalid_request_error` でリクエストが失敗します。エミュレーションを使うモデルでは pin は黙って捨てられ、推論シグナルのないままリクエストが +送られます。これらの Claude id のいずれかをネイティブ effort に上げた後では、以前は捨てられていた +`ultra` の pin が失敗します。`max` を pin してください。 + `claude-haiku-4.5`、`claude-opus-4.5`、`claude-sonnet-4.5` と、上記にない Kiro モデルは すべての effort でエミュレーションを使います。effort の選択肢だけではネイティブ対応を意味しません。 diff --git a/docs-site/src/content/docs/ko/reference/adapters.md b/docs-site/src/content/docs/ko/reference/adapters.md index abd6ca64c9a..7d8efb5e75c 100644 --- a/docs-site/src/content/docs/ko/reference/adapters.md +++ b/docs-site/src/content/docs/ko/reference/adapters.md @@ -223,8 +223,17 @@ Kiro는 `claude-opus-5`, `claude-opus-5.5`, `claude-opus-4.8`, `claude-opus-4.7` Kiro에는 `minimal` 단계가 없습니다. `claude-opus-5`와 `claude-opus-5.5`를 뺀 위의 아홉 모델은 네이티브 effort를 쓰기 전에도 동작하던 요청을 실패시키지 않으려고 `minimal`에서 에뮬레이션 블록을 유지합니다. 이 두 Opus 5 모델과 네이티브 effort를 쓰는 GPT 모델은 `minimal`을 모델과 effort를 -밝히는 400 `invalid_request_error`로 거부합니다. `ultra`는 Kiro에 도달하지 않습니다. Responses와 -Messages 경로 모두 어댑터가 요청을 만들기 전에 `max`로 낮춥니다. +밝히는 400 `invalid_request_error`로 거부합니다. + +`ultra`는 구분이 필요합니다. 호출자가 보낸 `ultra`는 Responses와 Messages 경로 모두 어댑터가 +요청을 만들기 전에 `max`로 낮춥니다. 설정한 pin은 다릅니다. `pinnedReasoningEffort`, +`modelPinnedReasoningEfforts`, `modelPinnedEfforts`는 `ultra`를 받아들이고 요청을 파싱한 뒤에 +적용하므로 어댑터까지 도달합니다. 네이티브 Kiro effort를 쓰는 모든 모델, 즉 위에 나온 Claude id와 +GPT 모델에서는 이 pin이 Kiro가 받지 않는 단계라서 모델과 effort를 밝히는 400 +`invalid_request_error`로 요청이 실패합니다. 에뮬레이션을 쓰는 모델에서는 pin이 +조용히 버려지고 추론 신호 없이 요청이 나갑니다. 이 Claude id 중 하나를 네이티브 effort로 올린 +뒤라면, 전에는 버려지던 `ultra` pin이 이제 실패합니다. `max`를 pin하십시오. + `claude-haiku-4.5`, `claude-opus-4.5`, `claude-sonnet-4.5`와 위에 없는 Kiro 모델은 모든 effort에서 에뮬레이션을 씁니다. 조절 항목이 있다고 네이티브 지원을 뜻하지는 않습니다. diff --git a/docs-site/src/content/docs/reference/adapters.md b/docs-site/src/content/docs/reference/adapters.md index 2d18bd3fc74..317fdb93105 100644 --- a/docs-site/src/content/docs/reference/adapters.md +++ b/docs-site/src/content/docs/reference/adapters.md @@ -591,7 +591,8 @@ Use `liveModels: false` for a static catalog; Settings can switch between the tw - 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 the PR does not report them. Every other Kiro + 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. @@ -750,9 +751,19 @@ 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` naming the model and the effort. `ultra` never -reaches Kiro: the Responses and Messages paths both reduce it to `max` before the adapter builds -a request. `claude-haiku-4.5`, `claude-opus-4.5` and `claude-sonnet-4.5`, along with any Kiro +`minimal` with a 400 `invalid_request_error` naming the model and the effort. + +`ultra` needs a distinction. A caller's own `ultra` is reduced to `max` before the adapter builds +a request, on both the Responses and Messages paths. A configured pin is not: `pinnedReasoningEffort`, +`modelPinnedReasoningEfforts` and `modelPinnedEfforts` accept `ultra` and are applied after the +request is parsed, so the adapter does receive it. For every model with native Kiro effort — the +Claude ids above and the GPT models — that pin fails the request with a 400 +`invalid_request_error` naming the model and the effort, because `ultra` is not a rung Kiro +accepts. For a model that uses emulated reasoning the pin is dropped instead, and the request goes +out with no reasoning signal at all. If you upgraded onto native effort for one of these Claude +ids, a pin of `ultra` that used to be dropped now fails: pin `max` instead. + +`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. diff --git a/docs-site/src/content/docs/ru/reference/adapters.md b/docs-site/src/content/docs/ru/reference/adapters.md index ab5d7c58734..a40b3ed5d5f 100644 --- a/docs-site/src/content/docs/ru/reference/adapters.md +++ b/docs-site/src/content/docs/ru/reference/adapters.md @@ -228,8 +228,19 @@ Kiro принимает `output_config.effort` для `claude-opus-5`, `claude-o У Kiro нет уровня `minimal`. На девяти моделях выше, кроме `claude-opus-5` и `claude-opus-5.5`, `minimal` сохраняет эмулируемый блок вместо отказа запросу, который работал до включения нативного effort. Эти две модели Opus 5, а также модели GPT с нативным effort, отклоняют `minimal` ошибкой -400 `invalid_request_error` с указанием модели и уровня. `ultra` до Kiro не доходит: и путь -Responses, и путь Messages сводят его к `max` до того, как адаптер построит запрос. +400 `invalid_request_error` с указанием модели и уровня. + +`ultra` требует оговорки. Присланный вызывающей стороной `ultra` сводится к `max` до того, как +адаптер построит запрос, — и на пути Responses, и на пути Messages. Настроенный pin ведёт себя +иначе: `pinnedReasoningEffort`, `modelPinnedReasoningEfforts` и `modelPinnedEfforts` принимают +`ultra` и применяются уже после разбора запроса, поэтому адаптер его получает. Для каждой модели с +нативным Kiro effort — перечисленных выше идентификаторов Claude и моделей GPT — такой pin +завершает запрос ошибкой 400 `invalid_request_error` с указанием модели и уровня, поскольку +`ultra` — не тот уровень, который принимает Kiro. Для модели на эмуляции +pin просто отбрасывается, и запрос уходит вообще без сигнала рассуждений. Если вы перевели один из +этих идентификаторов Claude на нативный effort, pin `ultra`, который раньше отбрасывался, теперь +завершается ошибкой: закрепляйте `max`. + `claude-haiku-4.5`, `claude-opus-4.5`, `claude-sonnet-4.5` и любая не перечисленная выше модель Kiro используют эмуляцию на каждом уровне; наличие настройки effort не доказывает нативную поддержку. diff --git a/docs-site/src/content/docs/tr/reference/adapters.md b/docs-site/src/content/docs/tr/reference/adapters.md index 2b1b1844821..9ce5529af46 100644 --- a/docs-site/src/content/docs/tr/reference/adapters.md +++ b/docs-site/src/content/docs/tr/reference/adapters.md @@ -231,7 +231,7 @@ model listesi kanıtı bulunmayabilir. `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ı o modellere göndermedi. Diğer Claude + 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 @@ -317,8 +317,19 @@ Kiro'da `minimal` seviyesi yoktur. Yukarıdaki dokuz modelden `claude-opus-5` ve 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 modeli ve seviyeyi belirten 400 `invalid_request_error` ile reddeder. -`ultra` Kiro'ya hiç ulaşmaz: hem Responses hem Messages yolu, adaptör isteği kurmadan önce onu -`max` seviyesine indirir. `claude-haiku-4.5`, `claude-opus-4.5`, `claude-sonnet-4.5` ve + +`ultra` için bir ayrım gerekir. Çağıranın kendi gönderdiği `ultra`, hem Responses hem Messages +yolunda adaptör isteği kurmadan önce `max` seviyesine indirilir. Yapılandırılmış pin ise böyle +değildir: `pinnedReasoningEffort`, `modelPinnedReasoningEfforts` ve `modelPinnedEfforts` `ultra` +değerini kabul eder ve istek ayrıştırıldıktan sonra uygulanır, dolayısıyla adaptör onu gerçekten +alır. Yerel Kiro çabası kullanan her modelde, yani yukarıdaki Claude kimliklerinde ve GPT +modellerinde, bu pin `ultra` Kiro'nun kabul ettiği bir seviye olmadığı için isteği modeli ve +seviyeyi belirten 400 `invalid_request_error` ile başarısız kılar. Öykünme kullanan bir modelde ise +pin sessizce düşürülür ve istek hiçbir akıl yürütme sinyali olmadan gider. Bu Claude +kimliklerinden birini yerel çabaya yükselttiyseniz, önceden düşürülen bir `ultra` pin artık +başarısız olur: `max` pinleyin. + +`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. 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 eb799512b71..785bf5352aa 100644 --- a/docs-site/src/content/docs/zh-cn/reference/adapters.md +++ b/docs-site/src/content/docs/zh-cn/reference/adapters.md @@ -198,8 +198,16 @@ Kiro 对 `claude-opus-5`、`claude-opus-5.5`、`claude-opus-4.8`、`claude-opus- Kiro 没有 `minimal` 档位。除 `claude-opus-5` 和 `claude-opus-5.5` 外的上述九个模型在 `minimal` 时保留模拟块,而不是让启用原生 effort 之前可用的请求失败。这两个 Opus 5 模型以及使用 原生 effort 的 GPT 模型会以 400 `invalid_request_error` 拒绝 `minimal`,并在错误中标明模型和 -effort。`ultra` 永远不会到达 Kiro:Responses 和 Messages 路径都会在适配器构建请求前把它降为 -`max`。`claude-haiku-4.5`、`claude-opus-4.5`、`claude-sonnet-4.5` 以及上面未列出的 Kiro +effort。 + +`ultra` 需要区分。调用方自己发送的 `ultra` 会在适配器构建请求前被降为 `max`,Responses 和 +Messages 路径都是如此。配置的 pin 则不同:`pinnedReasoningEffort`、`modelPinnedReasoningEfforts` +和 `modelPinnedEfforts` 接受 `ultra`,并在请求解析之后才应用,因此会到达适配器。对所有使用原生 +Kiro effort 的模型(即上面的 Claude id 与 GPT 模型),这个 pin 都不是 Kiro 接受的档位,请求会以 +400 `invalid_request_error` 失败,并在错误中标明模型和 effort。对使用模拟推理的模型,pin 会被静默丢弃,请求在没有推理信号的情况下发出。如果你已把其中 +某个 Claude id 升级为原生 effort,那么以前会被丢弃的 `ultra` pin 现在会失败;请改用 `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 dfa7e487aa1..2402788fd97 100644 --- a/docs-site/src/content/docs/zh-tw/reference/adapters.md +++ b/docs-site/src/content/docs/zh-tw/reference/adapters.md @@ -178,8 +178,16 @@ Kiro 對 `claude-opus-5`、`claude-opus-5.5`、`claude-opus-4.8`、`claude-opus- Kiro 沒有 `minimal` 檔位。除 `claude-opus-5` 和 `claude-opus-5.5` 外的上述九個模型在 `minimal` 時保留模擬區塊,而不是讓啟用原生 effort 之前可用的請求失敗。這兩個 Opus 5 模型以及使用 原生 effort 的 GPT 模型會以 400 `invalid_request_error` 拒絕 `minimal`,並在錯誤中標明模型和 -effort。`ultra` 永遠不會到達 Kiro:Responses 和 Messages 路徑都會在適配器建置請求前把它降為 -`max`。`claude-haiku-4.5`、`claude-opus-4.5`、`claude-sonnet-4.5` 以及上面未列出的 Kiro +effort。 + +`ultra` 需要區分。呼叫方自己送出的 `ultra` 會在適配器建置請求前被降為 `max`,Responses 和 +Messages 路徑都是如此。設定的 pin 則不同:`pinnedReasoningEffort`、`modelPinnedReasoningEfforts` +和 `modelPinnedEfforts` 接受 `ultra`,並在請求解析之後才套用,因此會到達適配器。對所有使用原生 +Kiro effort 的模型(即上面的 Claude id 與 GPT 模型),這個 pin 都不是 Kiro 接受的檔位,請求會以 +400 `invalid_request_error` 失敗,並在錯誤中標明模型和 effort。對使用模擬推理的模型,pin 會被靜默丟棄,請求在沒有推理訊號的情況下送出。如果你已把其中 +某個 Claude id 升級為原生 effort,那麼以前會被丟棄的 `ultra` pin 現在會失敗;請改用 `max`。 + +`claude-haiku-4.5`、`claude-opus-4.5`、`claude-sonnet-4.5` 以及上面未列出的 Kiro 模型在每個 effort 上都使用模擬推理;提供 effort 選項不代表原生支援。 ## `cursor` diff --git a/src/adapters/kiro/reasoning.ts b/src/adapters/kiro/reasoning.ts index ee5e1131a4e..025cea2230a 100644 --- a/src/adapters/kiro/reasoning.ts +++ b/src/adapters/kiro/reasoning.ts @@ -53,12 +53,24 @@ const KIRO_LUNA_TERRA_NATIVE_EFFORTS = new Set(["low", "medium", "high", "max"]) // Kiro rejects a native xhigh for these two (HTTP 400, live probe 2026-11-07); it stays emulated. const KIRO_NO_XHIGH_NATIVE_MODELS = new Set(["claude-opus-4.6", "claude-sonnet-4.6"]); // These models used the emulated block for every effort before native effort was verified, so -// `minimal` — the only value outside the native ladder that reaches a Kiro request — keeps that -// block rather than becoming an error. `ultra` is not a case here: the Responses parser, the -// Messages translator and combo resolution all fold it to `max` before the adapter sees it. -// Models absent from this set still reject `minimal`: it is an enumerated compatibility set, not -// a general unknown-effort fallback. The value check is what keeps it that way: an effort this -// file has never seen still throws instead of silently dropping reasoning. +// `minimal` — the one value outside the native ladder that arrives as the caller's own request — +// keeps that block rather than becoming an error. Models absent from this set still reject +// `minimal`: it is an enumerated compatibility set, not a general unknown-effort fallback. The +// value check is what keeps it that way: an effort this file has never seen still throws instead +// of silently dropping reasoning. +// +// `ultra` is a separate case, and the inbound fold is not the whole story. Caller-supplied +// `ultra` is folded to `max` before the adapter — the Responses parser +// (`src/responses/parser.ts`) does it for both surfaces, because the Messages translator +// (`src/claude/inbound.ts`) copies `output_config.effort` through verbatim and then hands the +// translated body to that same parser — but an operator pin is applied later, in +// `applyPinnedEffort` (`src/server/effort-policy.ts`), which runs after parsing and accepts +// `ultra` because it is a declared effort. So a `pinnedReasoningEffort: "ultra"` — or a +// `modelPinnedReasoningEfforts` / `modelPinnedEfforts` value — does reach this file. Every model +// in the native table then throws, +// and the request becomes HTTP 400 `invalid_request_error` before anything is sent. Models +// outside the table keep the emulated path, where `kiroThinkingBudget` has no `ultra` entry, so +// the pin is dropped and no reasoning signal goes out at all. const KIRO_EMULATED_EXTRA_EFFORT_MODELS = new Set([ "claude-opus-4.8", "claude-opus-4.7", diff --git a/structure/providers/kiro.md b/structure/providers/kiro.md index 503ba1befcf..27abbe857dd 100644 --- a/structure/providers/kiro.md +++ b/structure/providers/kiro.md @@ -366,15 +366,37 @@ 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. `minimal` is the only -outside-ladder value that reaches a Kiro request: the Responses parser folds `ultra` to `max`, as -does the Messages translator, 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`, +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 not a caller request, and it is not folded. `applyPinnedEffort` +(`src/server/effort-policy.ts`) runs after the body is parsed and writes the configured value over +the folded one; it accepts `ultra` because `pinnedReasoningEffort` and both pinned maps validate +against `isDeclaredReasoningEffort`, which includes the whole Codex ladder. So a pin of `ultra` +does reach `buildKiroPayload`, and what happens there depends on the model: + +- A model in `KIRO_NATIVE_EFFORT_FIELDS` throws, because `ultra` is outside `KIRO_NATIVE_EFFORTS` + and `kiroEmulatesUnsupportedEffort` only ever admits `minimal`. The request becomes HTTP 400 + `invalid_request_error` naming the model and the effort, on both the Responses and the Messages + surfaces, and nothing is sent upstream. +- A model outside the table keeps the emulated path. `kiroThinkingBudget` has no `ultra` entry, so + the pin is silently dropped and the request goes out with no reasoning signal — neither the + native field nor the thinking block. + +This is a real upgrade effect for the seven Claude ids above, not an accident of this table. On a +base that predates them they were outside the native table, so a pinned `ultra` was dropped the +same way `claude-haiku-4.5`, `claude-opus-4.5`, `claude-sonnet-4.5` and the non-Claude emulated +models still are. Becoming native on this branch turned that silent drop into a 400. The GPT +models and Opus 5 / Opus 5.5 already rejected a pinned `ultra` on that base, so nothing changed +for them. `tests/providers/kiro/kiro-pinned-effort-ultra.test.ts` holds both halves of the split. + 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` naming the model and the effort. This is an enumerated fallback, not a 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..4d5fcfd5cc4 --- /dev/null +++ b/tests/providers/kiro/kiro-pinned-effort-ultra.test.ts @@ -0,0 +1,167 @@ +import { afterEach, beforeEach, expect, test } from "bun:test"; +import { mkdtempSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { KIRO_NATIVE_EFFORT_FIELDS } from "../../../src/adapters/kiro/reasoning"; +import { getAccountSet, saveCredential, setActiveAccount } from "../../../src/oauth/store"; +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. */ +function config(models: string[], pin?: string): OcxConfig { + return { port: 0, defaultProvider: "kiro", providers: { kiro: { + adapter: "kiro", authMode: "oauth", baseUrl: "https://runtime.us-east-1.kiro.dev", + models, ...(pin ? { pinnedReasoningEffort: pin } : {}), + } } } 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: string | undefined) { + 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: false, + ...(effort ? { reasoning: { effort } } : {}), + }), + }), config([model], pin), { model, provider: "kiro" }); + const body = await response.json() as { error?: { message?: string; type?: string } }; + return { status: response.status, error: body.error, wires: captured.bodies }; +} + +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; + +// The claim this file replaces: "ultra never reaches the adapter". It reaches the adapter whenever +// an operator pin supplies it, because pins are applied AFTER the inbound fold — the Responses +// parser turns a caller's `ultra` into `max` (src/responses/parser.ts) and the Messages translator +// copies a caller's `output_config.effort` through (src/claude/inbound.ts), but +// `applyPinnedEffort` (src/server/effort-policy.ts) writes the configured value last. +test("an operator pin of ultra reaches the adapter, which admits only its own ladder", async () => { + const native = await responsesRequest("claude-opus-4.8", "high", "ultra"); + expect(native.status).toBe(400); + expect(native.error?.type).toBe("invalid_request_error"); + expect(native.error?.message).toContain('claude-opus-4.8 does not support reasoning effort "ultra"'); + // Nothing was sent: the refusal happens while the payload is built, not after an upstream round trip. + expect(native.wires).toHaveLength(0); + + // The same request without the pin is folded to `max` and does reach the wire. + const folded = await responsesRequest("claude-opus-4.8", "ultra", undefined); + expect(folded.status).toBe(500); + expect(folded.wires).toHaveLength(1); + expect(reasoningField(folded.wires[0])).toEqual({ output_config: { effort: "max" } }); +}); + +test("every model with a native Kiro effort field rejects a pinned ultra instead of sending it", async () => { + const models = Object.keys(KIRO_NATIVE_EFFORT_FIELDS); + // Guard the enumeration itself: an empty table would make the loop below pass vacuously. + expect(models.length).toBeGreaterThan(0); + for (const model of models) { + const { status, error, wires } = await responsesRequest(model, undefined, "ultra"); + expect({ model, status }).toEqual({ model, status: 400 }); + expect({ model, message: error?.message }).toEqual({ + model, message: `Kiro ${model} does not support reasoning effort "ultra"`, + }); + expect({ model, sends: wires.length }).toEqual({ model, 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 applies the same pin and answers with an Anthropic 400", async () => { + 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, + messages: [{ role: "user", content: "hi" }], + }), + }), config(["claude-opus-4.8"], "ultra"), { model: "", provider: "" } as never); + const body = await response.json() as { type?: string; error?: { type?: string } }; + expect(response.status).toBe(400); + expect(body.type).toBe("error"); + expect(body.error?.type).toBe("invalid_request_error"); +}); + +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" } }); +}); From 4ed8ecd16ec668bdb7ad094b57a446fc59d3c039 Mon Sep 17 00:00:00 2001 From: JUN Date: Fri, 13 Nov 2026 03:04:42 +0900 Subject: [PATCH 14/17] test(kiro): register the pinned-effort ultra test in both layout tables tests/providers/kiro/kiro-pinned-effort-ultra.test.ts landed on the branch with its subject, but only the regex seeds placed it: kiro- resolves to providers/kiro, so the guards stayed green while the authoritative table and its oracle never named the file. AGENTS.md requires an explicit registration in scripts/test-layout/layout.json explicit (or a domain shard) and in a tests/fixtures/test-layout-expected*.json shard even when a seed would place the file anyway, because a seed-resolved regression test is invisible to the inventory. Both entries are added on the existing kiro-* line of their file, so neither grows a line and the file-size ratchet cap is unchanged. Verified on Bun 1.4.4 with a temporary HOME and OPENCODEX_HOME: bun test tests/test-layout.test.ts tests/test-layout-tooling.test.ts (29 pass), bun test on the pinned-effort test plus its two catalog neighbors (34 pass), bun scripts/file-size-ratchet.ts, bun run typecheck, bun run structure:check. Co-authored-by: svc-aibot-363 <271381517+svc-aibot-363@users.noreply.github.com> --- scripts/test-layout/layout.json | 2 +- tests/fixtures/test-layout-expected.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/test-layout/layout.json b/scripts/test-layout/layout.json index f68d4b531b5..fcc3d6f00d7 100644 --- a/scripts/test-layout/layout.json +++ b/scripts/test-layout/layout.json @@ -1089,7 +1089,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", diff --git a/tests/fixtures/test-layout-expected.json b/tests/fixtures/test-layout-expected.json index 49817b1afa3..78a26055486 100644 --- a/tests/fixtures/test-layout-expected.json +++ b/tests/fixtures/test-layout-expected.json @@ -981,7 +981,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", From 38e3d1f3b8b4ef38a3298f92ae50c9581368e5a3 Mon Sep 17 00:00:00 2001 From: JUN Date: Fri, 13 Nov 2026 05:23:43 +0900 Subject: [PATCH 15/17] test(kiro): pin the per-surface refusal copy a rejected pin produces The docs claimed a rejected minimal or pinned ultra names the model and the effort on both surfaces. Only Responses does: Messages and Chat Completions replace the adapter detail with the fixed request_rejected sentence ("The provider rejected the request.") while keeping the 400 invalid_request_error type, on stream and non-stream requests alike. Reproduced on the real handleResponses, handleClaudeMessages and handleChatCompletions routes with a mocked fetch and an isolated home: the adapter throw happens while the wire payload is built, so the upstream call count stays 0 and the refusal is JSON rather than an opened stream. Docs (English plus seven translations), structure/providers/kiro.md and the surrounding comments now describe each surface's body correctly and keep the native/emulated/upgrade distinctions. Runtime and security redaction are unchanged. The test pins the exact Responses detail, the exact Messages body and the exact Chat Completions body across both stream modes, and asserts no upstream call. Co-authored-by: svc-aibot-363 <271381517+svc-aibot-363@users.noreply.github.com> --- .../src/content/docs/fr/reference/adapters.md | 24 ++- .../src/content/docs/ja/reference/adapters.md | 20 ++- .../src/content/docs/ko/reference/adapters.md | 21 ++- .../src/content/docs/reference/adapters.md | 20 ++- .../src/content/docs/ru/reference/adapters.md | 20 ++- .../src/content/docs/tr/reference/adapters.md | 20 ++- .../content/docs/zh-cn/reference/adapters.md | 17 +- .../content/docs/zh-tw/reference/adapters.md | 17 +- structure/providers/kiro.md | 31 ++-- .../kiro/kiro-pinned-effort-ultra.test.ts | 153 +++++++++++++++--- 10 files changed, 261 insertions(+), 82 deletions(-) diff --git a/docs-site/src/content/docs/fr/reference/adapters.md b/docs-site/src/content/docs/fr/reference/adapters.md index 98383f7e1d9..25808f5a7b3 100644 --- a/docs-site/src/content/docs/fr/reference/adapters.md +++ b/docs-site/src/content/docs/fr/reference/adapters.md @@ -161,19 +161,29 @@ 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` -nommant le modèle et le niveau. +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. `ultra` demande une distinction. Le `ultra` envoyé par l’appelant est réduit à `max` avant que l’adaptateur ne construise la requête, sur les chemins Responses et Messages. Un pin configuré, non : `pinnedReasoningEffort`, `modelPinnedReasoningEfforts` et `modelPinnedEfforts` acceptent `ultra` et s’appliquent après l’analyse de la requête, donc l’adaptateur le reçoit. Pour chaque modèle à effort natif Kiro — les identifiants Claude ci-dessus et les modèles GPT — ce pin fait échouer la requête -par une erreur 400 `invalid_request_error` nommant le modèle et le niveau, car `ultra` n’est pas un -palier que Kiro accepte. Pour un modèle qui utilise -l’émulation, le pin est abandonné et la requête part sans aucun signal de raisonnement. Si vous avez -fait passer l’un de ces identifiants Claude en effort natif, un pin `ultra` autrefois ignoré échoue -désormais : épinglez `max`. +par une erreur 400 `invalid_request_error` avant tout envoi, car `ultra` n’est pas un palier que +Kiro accepte. Ce refus se répartit entre les surfaces exactement comme `minimal` : le corps +Responses nomme le modèle et le niveau, les corps Messages et Chat Completions ne portent que le +libellé fixe du proxy et ne nomment ni l’un ni l’autre. +Pour un modèle qui utilise l’émulation, le pin est abandonné et la requête part sans aucun signal de +raisonnement. Si vous avez fait passer l’un de ces identifiants Claude en effort natif, un pin +`ultra` autrefois ignoré échoue désormais : épinglez `max`. `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 diff --git a/docs-site/src/content/docs/ja/reference/adapters.md b/docs-site/src/content/docs/ja/reference/adapters.md index 61afc2d572a..980943c5bef 100644 --- a/docs-site/src/content/docs/ja/reference/adapters.md +++ b/docs-site/src/content/docs/ja/reference/adapters.md @@ -190,17 +190,27 @@ Kiro は `claude-opus-5`、`claude-opus-5.5`、`claude-opus-4.8`、`claude-opus- Kiro には `minimal` の段階がありません。`claude-opus-5` と `claude-opus-5.5` を除く上記の 9 モデルは、 ネイティブ effort を有効にする前から動作していたリクエストを失敗させないよう、`minimal` でエミュレーションの -ブロックを維持します。この 2 つの Opus 5 モデルとネイティブ effort を持つ GPT 系は、`minimal` を -モデルと effort を示す 400 `invalid_request_error` で拒否します。 +ブロックを維持します。この 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` には区別が必要です。呼び出し元が送った `ultra` は、Responses と Messages のどちらの経路でも アダプターがリクエストを組み立てる前に `max` へ下げます。設定した pin は違います。 `pinnedReasoningEffort`、`modelPinnedReasoningEfforts`、`modelPinnedEfforts` は `ultra` を受け付け、 リクエストを解析した後に適用するため、アダプターまで届きます。ネイティブ Kiro effort を持つすべての モデル、つまり上記の Claude id と GPT モデルでは、この pin が Kiro の受け付けない段階であるため、 -モデルと effort を示す 400 `invalid_request_error` でリクエストが失敗します。エミュレーションを使うモデルでは pin は黙って捨てられ、推論シグナルのないままリクエストが -送られます。これらの Claude id のいずれかをネイティブ effort に上げた後では、以前は捨てられていた -`ultra` の pin が失敗します。`max` を pin してください。 +何も送信する前に 400 `invalid_request_error` でリクエストが失敗します。この拒否も `minimal` と +同じくサーフェスごとに分かれます。Responses の本文はモデルと effort を示し、Messages と Chat +Completions の本文はプロキシの固定文言だけを載せ、どちらも示しません。エミュレーションを使う +モデルでは pin は黙って捨てられ、推論シグナルのないままリクエストが送られます。これらの Claude id +のいずれかをネイティブ effort に上げた後では、以前は捨てられていた `ultra` の pin が失敗します。 +`max` を pin してください。 `claude-haiku-4.5`、`claude-opus-4.5`、`claude-sonnet-4.5` と、上記にない Kiro モデルは すべての effort でエミュレーションを使います。effort の選択肢だけではネイティブ対応を意味しません。 diff --git a/docs-site/src/content/docs/ko/reference/adapters.md b/docs-site/src/content/docs/ko/reference/adapters.md index 7d8efb5e75c..30ed11451e7 100644 --- a/docs-site/src/content/docs/ko/reference/adapters.md +++ b/docs-site/src/content/docs/ko/reference/adapters.md @@ -222,17 +222,26 @@ Kiro는 `claude-opus-5`, `claude-opus-5.5`, `claude-opus-4.8`, `claude-opus-4.7` Kiro에는 `minimal` 단계가 없습니다. `claude-opus-5`와 `claude-opus-5.5`를 뺀 위의 아홉 모델은 네이티브 effort를 쓰기 전에도 동작하던 요청을 실패시키지 않으려고 `minimal`에서 에뮬레이션 블록을 -유지합니다. 이 두 Opus 5 모델과 네이티브 effort를 쓰는 GPT 모델은 `minimal`을 모델과 effort를 -밝히는 400 `invalid_request_error`로 거부합니다. +유지합니다. 이 두 Opus 5 모델과 네이티브 effort를 쓰는 GPT 모델은 `minimal`을 아무것도 보내기 전에 +400 `invalid_request_error`로 거부합니다. + +거부를 알리는 방식은 표면마다 다릅니다. Responses 요청은 모델과 거부된 effort를 밝히는 문장을 +`error.message`에 담습니다. Messages와 Chat Completions 경로는 그 자리에 프록시의 고정 문구인 +`The provider rejected the request.`를 넣습니다. 두 경로 모두 어댑터의 세부 문장을 그대로 전달하지 +않고 자기 문구로 바꾸기 때문입니다. 모델과 effort를 밝히는 쪽은 Responses뿐입니다. 스트리밍과 +비스트리밍은 모든 표면에서 같은 응답을 냅니다. 거부는 업스트림 요청 본문을 만드는 도중에 일어나므로 +스트림이 열리지 않고 업스트림 호출도 없습니다. `ultra`는 구분이 필요합니다. 호출자가 보낸 `ultra`는 Responses와 Messages 경로 모두 어댑터가 요청을 만들기 전에 `max`로 낮춥니다. 설정한 pin은 다릅니다. `pinnedReasoningEffort`, `modelPinnedReasoningEfforts`, `modelPinnedEfforts`는 `ultra`를 받아들이고 요청을 파싱한 뒤에 적용하므로 어댑터까지 도달합니다. 네이티브 Kiro effort를 쓰는 모든 모델, 즉 위에 나온 Claude id와 -GPT 모델에서는 이 pin이 Kiro가 받지 않는 단계라서 모델과 effort를 밝히는 400 -`invalid_request_error`로 요청이 실패합니다. 에뮬레이션을 쓰는 모델에서는 pin이 -조용히 버려지고 추론 신호 없이 요청이 나갑니다. 이 Claude id 중 하나를 네이티브 effort로 올린 -뒤라면, 전에는 버려지던 `ultra` pin이 이제 실패합니다. `max`를 pin하십시오. +GPT 모델에서는 이 pin이 Kiro가 받지 않는 단계라서 아무것도 보내기 전에 400 +`invalid_request_error`로 요청이 실패합니다. 이 거부도 `minimal`과 똑같이 표면별로 나뉩니다. +Responses 본문은 모델과 effort를 밝히고, Messages와 Chat Completions 본문은 프록시의 고정 문구만 +담으며 둘 다 밝히지 않습니다. 에뮬레이션을 쓰는 모델에서는 pin이 조용히 버려지고 추론 신호 없이 +요청이 나갑니다. 이 Claude id 중 하나를 네이티브 effort로 올린 뒤라면, 전에는 버려지던 `ultra` +pin이 이제 실패합니다. `max`를 pin하십시오. `claude-haiku-4.5`, `claude-opus-4.5`, `claude-sonnet-4.5`와 위에 없는 Kiro 모델은 모든 effort에서 에뮬레이션을 씁니다. 조절 항목이 있다고 네이티브 지원을 뜻하지는 않습니다. diff --git a/docs-site/src/content/docs/reference/adapters.md b/docs-site/src/content/docs/reference/adapters.md index 317fdb93105..4c2bb412bb9 100644 --- a/docs-site/src/content/docs/reference/adapters.md +++ b/docs-site/src/content/docs/reference/adapters.md @@ -751,17 +751,27 @@ 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` naming the model and the effort. +`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. `ultra` needs a distinction. A caller's own `ultra` is reduced to `max` before the adapter builds a request, on both the Responses and Messages paths. A configured pin is not: `pinnedReasoningEffort`, `modelPinnedReasoningEfforts` and `modelPinnedEfforts` accept `ultra` and are applied after the request is parsed, so the adapter does receive it. For every model with native Kiro effort — the Claude ids above and the GPT models — that pin fails the request with a 400 -`invalid_request_error` naming the model and the effort, because `ultra` is not a rung Kiro -accepts. For a model that uses emulated reasoning the pin is dropped instead, and the request goes -out with no reasoning signal at all. If you upgraded onto native effort for one of these Claude -ids, a pin of `ultra` that used to be dropped now fails: pin `max` instead. +`invalid_request_error` before anything is sent, because `ultra` is not a rung Kiro accepts. That +refusal is split across the surfaces exactly as `minimal` is: the Responses body names the model +and the effort, while the Messages and Chat Completions bodies carry only the fixed condition copy +and name neither. +For a model that uses emulated reasoning the pin is dropped instead, and the request goes out with +no reasoning signal at all. If you upgraded onto native effort for one of these Claude ids, a pin of +`ultra` that used to be dropped now fails: pin `max` instead. `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 diff --git a/docs-site/src/content/docs/ru/reference/adapters.md b/docs-site/src/content/docs/ru/reference/adapters.md index a40b3ed5d5f..ebb3fe39b56 100644 --- a/docs-site/src/content/docs/ru/reference/adapters.md +++ b/docs-site/src/content/docs/ru/reference/adapters.md @@ -228,18 +228,26 @@ Kiro принимает `output_config.effort` для `claude-opus-5`, `claude-o У Kiro нет уровня `minimal`. На девяти моделях выше, кроме `claude-opus-5` и `claude-opus-5.5`, `minimal` сохраняет эмулируемый блок вместо отказа запросу, который работал до включения нативного effort. Эти две модели Opus 5, а также модели GPT с нативным effort, отклоняют `minimal` ошибкой -400 `invalid_request_error` с указанием модели и уровня. +400 `invalid_request_error` до отправки чего-либо. + +Поверхности сообщают об отказе по-разному. В запросе Responses в `error.message` попадает фраза с +названием модели и отклонённого уровня. На путях Messages и Chat Completions на этом месте стоит +фиксированный текст прокси `The provider rejected the request.`: оба пути заменяют подробность +адаптера собственным сообщением, а не передают её. Модель и уровень называет только Responses. +Потоковые и непоточные запросы читаются одинаково на всех поверхностях: отказ возникает при сборке +тела запроса к провайдеру, поэтому поток не открывается и вызов провайдера не происходит. `ultra` требует оговорки. Присланный вызывающей стороной `ultra` сводится к `max` до того, как адаптер построит запрос, — и на пути Responses, и на пути Messages. Настроенный pin ведёт себя иначе: `pinnedReasoningEffort`, `modelPinnedReasoningEfforts` и `modelPinnedEfforts` принимают `ultra` и применяются уже после разбора запроса, поэтому адаптер его получает. Для каждой модели с нативным Kiro effort — перечисленных выше идентификаторов Claude и моделей GPT — такой pin -завершает запрос ошибкой 400 `invalid_request_error` с указанием модели и уровня, поскольку -`ultra` — не тот уровень, который принимает Kiro. Для модели на эмуляции -pin просто отбрасывается, и запрос уходит вообще без сигнала рассуждений. Если вы перевели один из -этих идентификаторов Claude на нативный effort, pin `ultra`, который раньше отбрасывался, теперь -завершается ошибкой: закрепляйте `max`. +завершает запрос ошибкой 400 `invalid_request_error` до отправки чего-либо, поскольку `ultra` — +не тот уровень, который принимает Kiro. Этот отказ делится между поверхностями так же, как +`minimal`: тело Responses называет модель и уровень, тела Messages и Chat Completions несут только +фиксированный текст прокси и не называют ни того, ни другого. Для модели на эмуляции pin просто отбрасывается, и запрос уходит вообще без сигнала +рассуждений. Если вы перевели один из этих идентификаторов Claude на нативный effort, pin `ultra`, +который раньше отбрасывался, теперь завершается ошибкой: закрепляйте `max`. `claude-haiku-4.5`, `claude-opus-4.5`, `claude-sonnet-4.5` и любая не перечисленная выше модель Kiro используют эмуляцию на каждом уровне; наличие настройки effort не доказывает нативную diff --git a/docs-site/src/content/docs/tr/reference/adapters.md b/docs-site/src/content/docs/tr/reference/adapters.md index 9ce5529af46..1b4a1725c28 100644 --- a/docs-site/src/content/docs/tr/reference/adapters.md +++ b/docs-site/src/content/docs/tr/reference/adapters.md @@ -316,18 +316,26 @@ 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 modeli ve seviyeyi belirten 400 `invalid_request_error` ile reddeder. +`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. `ultra` için bir ayrım gerekir. Çağıranın kendi gönderdiği `ultra`, hem Responses hem Messages yolunda adaptör isteği kurmadan önce `max` seviyesine indirilir. Yapılandırılmış pin ise böyle değildir: `pinnedReasoningEffort`, `modelPinnedReasoningEfforts` ve `modelPinnedEfforts` `ultra` değerini kabul eder ve istek ayrıştırıldıktan sonra uygulanır, dolayısıyla adaptör onu gerçekten alır. Yerel Kiro çabası kullanan her modelde, yani yukarıdaki Claude kimliklerinde ve GPT -modellerinde, bu pin `ultra` Kiro'nun kabul ettiği bir seviye olmadığı için isteği modeli ve -seviyeyi belirten 400 `invalid_request_error` ile başarısız kılar. Öykünme kullanan bir modelde ise -pin sessizce düşürülür ve istek hiçbir akıl yürütme sinyali olmadan gider. Bu Claude -kimliklerinden birini yerel çabaya yükselttiyseniz, önceden düşürülen bir `ultra` pin artık -başarısız olur: `max` pinleyin. +modellerinde, bu pin `ultra` Kiro'nun kabul ettiği bir seviye olmadığı için isteği hiçbir şey +gönderilmeden önce 400 `invalid_request_error` ile başarısız kılar. Bu ret de `minimal` gibi yüzeylere +ayrılır: Responses gövdesi modeli ve seviyeyi belirtir, Messages ve Chat Completions gövdeleri yalnızca +proxy'nin sabit metnini taşır ve ikisini de belirtmez. Öykünme kullanan bir modelde ise pin sessizce düşürülür ve istek hiçbir akıl +yürütme sinyali olmadan gider. Bu Claude kimliklerinden birini yerel çabaya yükselttiyseniz, önceden +düşürülen bir `ultra` pin artık başarısız olur: `max` pinleyin. `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 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 785bf5352aa..812ae9e4de3 100644 --- a/docs-site/src/content/docs/zh-cn/reference/adapters.md +++ b/docs-site/src/content/docs/zh-cn/reference/adapters.md @@ -197,15 +197,22 @@ Kiro 对 `claude-opus-5`、`claude-opus-5.5`、`claude-opus-4.8`、`claude-opus- Kiro 没有 `minimal` 档位。除 `claude-opus-5` 和 `claude-opus-5.5` 外的上述九个模型在 `minimal` 时保留模拟块,而不是让启用原生 effort 之前可用的请求失败。这两个 Opus 5 模型以及使用 -原生 effort 的 GPT 模型会以 400 `invalid_request_error` 拒绝 `minimal`,并在错误中标明模型和 -effort。 +原生 effort 的 GPT 模型会在发送任何内容之前以 400 `invalid_request_error` 拒绝 `minimal`。 + +各接口表达这次拒绝的方式不同。Responses 请求把标明模型和被拒绝 effort 的句子放进 +`error.message`。Messages 与 Chat Completions 路径在该位置放代理自己的固定文案 +`The provider rejected the request.`,因为这两条路径不会转述适配器的细节,而是用自有文案替换。 +只有 Responses 会标明模型和 effort。流式与非流式在所有接口上返回相同内容:拒绝发生在构建上游 +请求体时,因此没有打开流,也没有发出上游调用。 `ultra` 需要区分。调用方自己发送的 `ultra` 会在适配器构建请求前被降为 `max`,Responses 和 Messages 路径都是如此。配置的 pin 则不同:`pinnedReasoningEffort`、`modelPinnedReasoningEfforts` 和 `modelPinnedEfforts` 接受 `ultra`,并在请求解析之后才应用,因此会到达适配器。对所有使用原生 -Kiro effort 的模型(即上面的 Claude id 与 GPT 模型),这个 pin 都不是 Kiro 接受的档位,请求会以 -400 `invalid_request_error` 失败,并在错误中标明模型和 effort。对使用模拟推理的模型,pin 会被静默丢弃,请求在没有推理信号的情况下发出。如果你已把其中 -某个 Claude id 升级为原生 effort,那么以前会被丢弃的 `ultra` pin 现在会失败;请改用 `max`。 +Kiro effort 的模型(即上面的 Claude id 与 GPT 模型),这个 pin 都不是 Kiro 接受的档位,请求会在 +发送任何内容之前以 400 `invalid_request_error` 失败。这次拒绝也像 `minimal` 一样按接口区分: +Responses 正文标明模型和 effort,Messages 与 Chat Completions 正文只带代理的固定文案,两者都不标明。对使用模拟推理的模型,pin 会被 +静默丢弃,请求在没有推理信号的情况下发出。如果你已把其中某个 Claude id 升级为原生 effort, +那么以前会被丢弃的 `ultra` pin 现在会失败;请改用 `max`。 `claude-haiku-4.5`、`claude-opus-4.5`、`claude-sonnet-4.5` 以及上面未列出的 Kiro 模型在每个 effort 上都使用模拟推理;提供 effort 选项并不代表原生支持。 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 2402788fd97..9ed30c95eb9 100644 --- a/docs-site/src/content/docs/zh-tw/reference/adapters.md +++ b/docs-site/src/content/docs/zh-tw/reference/adapters.md @@ -177,15 +177,22 @@ Kiro 對 `claude-opus-5`、`claude-opus-5.5`、`claude-opus-4.8`、`claude-opus- Kiro 沒有 `minimal` 檔位。除 `claude-opus-5` 和 `claude-opus-5.5` 外的上述九個模型在 `minimal` 時保留模擬區塊,而不是讓啟用原生 effort 之前可用的請求失敗。這兩個 Opus 5 模型以及使用 -原生 effort 的 GPT 模型會以 400 `invalid_request_error` 拒絕 `minimal`,並在錯誤中標明模型和 -effort。 +原生 effort 的 GPT 模型會在傳送任何內容之前以 400 `invalid_request_error` 拒絕 `minimal`。 + +各介面表達這次拒絕的方式不同。Responses 請求把標明模型與被拒絕 effort 的句子放進 +`error.message`。Messages 與 Chat Completions 路徑在該位置放代理自己的固定文案 +`The provider rejected the request.`,因為這兩條路徑不會轉述適配器的細節,而是用自有文案替換。 +只有 Responses 會標明模型與 effort。串流與非串流在所有介面上回傳相同內容:拒絕發生在建構上游 +請求體時,因此沒有開啟串流,也沒有發出上游呼叫。 `ultra` 需要區分。呼叫方自己送出的 `ultra` 會在適配器建置請求前被降為 `max`,Responses 和 Messages 路徑都是如此。設定的 pin 則不同:`pinnedReasoningEffort`、`modelPinnedReasoningEfforts` 和 `modelPinnedEfforts` 接受 `ultra`,並在請求解析之後才套用,因此會到達適配器。對所有使用原生 -Kiro effort 的模型(即上面的 Claude id 與 GPT 模型),這個 pin 都不是 Kiro 接受的檔位,請求會以 -400 `invalid_request_error` 失敗,並在錯誤中標明模型和 effort。對使用模擬推理的模型,pin 會被靜默丟棄,請求在沒有推理訊號的情況下送出。如果你已把其中 -某個 Claude id 升級為原生 effort,那麼以前會被丟棄的 `ultra` pin 現在會失敗;請改用 `max`。 +Kiro effort 的模型(即上面的 Claude id 與 GPT 模型),這個 pin 都不是 Kiro 接受的檔位,請求會在 +傳送任何內容之前以 400 `invalid_request_error` 失敗。這次拒絕也像 `minimal` 一樣按介面區分: +Responses 正文標明模型與 effort,Messages 與 Chat Completions 正文只帶代理的固定文案,兩者都不標明。 +對使用模擬推理的模型,pin 會被靜默丟棄,請求在沒有推理訊號的情況下送出。如果你已把其中某個 +Claude id 升級為原生 effort,那麼以前會被丟棄的 `ultra` pin 現在會失敗;請改用 `max`。 `claude-haiku-4.5`、`claude-opus-4.5`、`claude-sonnet-4.5` 以及上面未列出的 Kiro 模型在每個 effort 上都使用模擬推理;提供 effort 選項不代表原生支援。 diff --git a/structure/providers/kiro.md b/structure/providers/kiro.md index 27abbe857dd..d62317dd121 100644 --- a/structure/providers/kiro.md +++ b/structure/providers/kiro.md @@ -365,11 +365,13 @@ 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. `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 — +anything is sent, unless `kiroEmulatesUnsupportedEffort` accepts the model. The surface split +described below for a pinned `ultra` applies here unchanged, because it is the same throw on the +same path. `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. @@ -383,9 +385,16 @@ against `isDeclaredReasoningEffort`, which includes the whole Codex ladder. So a does reach `buildKiroPayload`, and what happens there depends on the model: - A model in `KIRO_NATIVE_EFFORT_FIELDS` throws, because `ultra` is outside `KIRO_NATIVE_EFFORTS` - and `kiroEmulatesUnsupportedEffort` only ever admits `minimal`. The request becomes HTTP 400 - `invalid_request_error` naming the model and the effort, on both the Responses and the Messages - surfaces, and nothing is sent upstream. + and `kiroEmulatesUnsupportedEffort` only ever admits `minimal`. The throw becomes HTTP 400 + `invalid_request_error` on every surface, and nothing is sent upstream. What the client reads + differs: on Responses the adapter's `buildKiroPayload` message survives into `error.message` as + `Kiro does not support reasoning effort "ultra"` + (`src/server/responses/adapter-dispatch.ts`), while Messages and Chat Completions replace it with + the fixed `request_rejected` copy `The provider rejected the request.` + (`upstreamConditionMessage` in `src/lib/upstream-condition.ts`, reached through + `ownedAnthropicErrorPayload` in `src/lib/messages-error-body.ts` and through `ownedChatErrorPayload` + in `src/lib/client-error-body.ts`) because those paths re-shape the error and do not forward the + adapter detail. - A model outside the table keeps the emulated path. `kiroThinkingBudget` has no `ultra` entry, so the pin is silently dropped and the request goes out with no reasoning signal — neither the native field nor the thinking block. @@ -399,9 +408,9 @@ for them. `tests/providers/kiro/kiro-pinned-effort-ultra.test.ts` holds both hal 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` naming the model and the effort. 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. +`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/providers/kiro/kiro-pinned-effort-ultra.test.ts b/tests/providers/kiro/kiro-pinned-effort-ultra.test.ts index 4d5fcfd5cc4..fb6b7684ccf 100644 --- a/tests/providers/kiro/kiro-pinned-effort-ultra.test.ts +++ b/tests/providers/kiro/kiro-pinned-effort-ultra.test.ts @@ -4,6 +4,7 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { KIRO_NATIVE_EFFORT_FIELDS } from "../../../src/adapters/kiro/reasoning"; 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"; @@ -57,17 +58,62 @@ function captureWire(): { bodies: Record[] } { return { bodies }; } -async function responsesRequest(model: string, effort: string | undefined, pin: string | undefined) { +async function responsesRequest( + model: string, effort: string | undefined, pin: string | 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: false, + 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 } }; - return { status: response.status, error: body.error, wires: captured.bodies }; + 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: string | 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: string | 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) => @@ -84,8 +130,10 @@ const userContent = (wire: Record | undefined) => (wire?.conver test("an operator pin of ultra reaches the adapter, which admits only its own ladder", async () => { const native = await responsesRequest("claude-opus-4.8", "high", "ultra"); expect(native.status).toBe(400); - expect(native.error?.type).toBe("invalid_request_error"); - expect(native.error?.message).toContain('claude-opus-4.8 does not support reasoning effort "ultra"'); + expect(native.error).toEqual({ + message: 'Kiro claude-opus-4.8 does not support reasoning effort "ultra"', + type: "invalid_request_error", code: "invalid_request_error", + }); // Nothing was sent: the refusal happens while the payload is built, not after an upstream round trip. expect(native.wires).toHaveLength(0); @@ -101,12 +149,59 @@ test("every model with a native Kiro effort field rejects a pinned ultra instead // Guard the enumeration itself: an empty table would make the loop below pass vacuously. expect(models.length).toBeGreaterThan(0); for (const model of models) { - const { status, error, wires } = await responsesRequest(model, undefined, "ultra"); - expect({ model, status }).toEqual({ model, status: 400 }); - expect({ model, message: error?.message }).toEqual({ - model, message: `Kiro ${model} does not support reasoning effort "ultra"`, - }); - expect({ model, sends: wires.length }).toEqual({ model, sends: 0 }); + for (const stream of [false, true]) { + const { status, error, wires, contentType } = await responsesRequest(model, undefined, "ultra", stream); + expect({ model, stream, status }).toEqual({ model, stream, status: 400 }); + // The Responses public contract carries the adapter's sentence verbatim, stream or not. + expect({ model, stream, error }).toEqual({ + model, stream, + error: { + message: `Kiro ${model} does not support reasoning effort "ultra"`, + type: "invalid_request_error", code: "invalid_request_error", + }, + }); + expect({ model, stream, sends: wires.length }).toEqual({ model, stream, sends: 0 }); + // A refusal raised before the stream opens is JSON on both request shapes, never SSE. + expect(contentType).toContain("application/json"); + } + } +}); + +test("Chat Completions answers the same refusals with its own fixed copy", async () => { + // The OpenAI-compatible body adds `param`/`code` around the same fixed sentence. That surface is + // asserted separately so a Messages-only change cannot silently leave it forwarding the detail. + for (const [model, pin] of [["gpt-5.6-sol", "ultra"], ["claude-opus-5.5", "minimal"]] as const) { + for (const stream of [false, true]) { + const { status, body, wires, contentType } = await chatRequest(model, pin, stream); + expect({ model, pin, stream, status }).toEqual({ model, pin, stream, status: 400 }); + expect({ model, pin, stream, body }).toEqual({ + model, pin, stream, + body: { + error: { + message: "The provider rejected the request.", type: "invalid_request_error", + param: null, code: "invalid_request_error", + }, + }, + }); + expect({ model, pin, stream, sends: wires.length }).toEqual({ model, pin, stream, sends: 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 }); + } } }); @@ -129,20 +224,26 @@ test("a model with no native field keeps its emulated path, so a pinned ultra is expect(userContent(minimal.wires[0])).toContain("enabled"); }); -test("the Messages surface applies the same pin and answers with an Anthropic 400", async () => { - 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, - messages: [{ role: "user", content: "hi" }], - }), - }), config(["claude-opus-4.8"], "ultra"), { model: "", provider: "" } as never); - const body = await response.json() as { type?: string; error?: { type?: string } }; - expect(response.status).toBe(400); - expect(body.type).toBe("error"); - expect(body.error?.type).toBe("invalid_request_error"); +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-4.8", "ultra"], ["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 () => { From 8c56d51a3d35ddac4e596821104257ec12214fc9 Mon Sep 17 00:00:00 2001 From: JUN Date: Fri, 13 Nov 2026 10:59:54 +0900 Subject: [PATCH 16/17] fix(kiro): preserve pinned ultra compatibility Co-authored-by: svc-aibot-363 <271381517+svc-aibot-363@users.noreply.github.com> --- .../src/content/docs/fr/reference/adapters.md | 16 +-- .../src/content/docs/ja/reference/adapters.md | 15 +-- .../src/content/docs/ko/reference/adapters.md | 14 +-- .../src/content/docs/reference/adapters.md | 17 +-- .../src/content/docs/ru/reference/adapters.md | 15 +-- .../src/content/docs/tr/reference/adapters.md | 15 +-- .../content/docs/zh-cn/reference/adapters.md | 11 +- .../content/docs/zh-tw/reference/adapters.md | 11 +- src/adapters/kiro/payload.ts | 3 +- src/adapters/kiro/reasoning.ts | 15 +-- structure/providers/kiro.md | 33 +---- .../kiro/kiro-pinned-effort-ultra.test.ts | 118 ++++++++---------- .../providers/kiro/kiro-tool-catalog.test.ts | 2 +- 13 files changed, 92 insertions(+), 193 deletions(-) diff --git a/docs-site/src/content/docs/fr/reference/adapters.md b/docs-site/src/content/docs/fr/reference/adapters.md index 25808f5a7b3..2ac70dc2091 100644 --- a/docs-site/src/content/docs/fr/reference/adapters.md +++ b/docs-site/src/content/docs/fr/reference/adapters.md @@ -172,18 +172,10 @@ niveau. Les requêtes en flux et hors flux se lisent à l’identique sur toutes survient pendant la construction du corps envoyé en amont, donc aucun flux ne s’ouvre et aucun appel amont n’a lieu. -`ultra` demande une distinction. Le `ultra` envoyé par l’appelant est réduit à `max` avant que -l’adaptateur ne construise la requête, sur les chemins Responses et Messages. Un pin configuré, non : -`pinnedReasoningEffort`, `modelPinnedReasoningEfforts` et `modelPinnedEfforts` acceptent `ultra` et -s’appliquent après l’analyse de la requête, donc l’adaptateur le reçoit. Pour chaque modèle à effort -natif Kiro — les identifiants Claude ci-dessus et les modèles GPT — ce pin fait échouer la requête -par une erreur 400 `invalid_request_error` avant tout envoi, car `ultra` n’est pas un palier que -Kiro accepte. Ce refus se répartit entre les surfaces exactement comme `minimal` : le corps -Responses nomme le modèle et le niveau, les corps Messages et Chat Completions ne portent que le -libellé fixe du proxy et ne nomment ni l’un ni l’autre. -Pour un modèle qui utilise l’émulation, le pin est abandonné et la requête part sans aucun signal de -raisonnement. Si vous avez fait passer l’un de ces identifiants Claude en effort natif, un pin -`ultra` autrefois ignoré échoue désormais : épinglez `max`. +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 diff --git a/docs-site/src/content/docs/ja/reference/adapters.md b/docs-site/src/content/docs/ja/reference/adapters.md index 980943c5bef..391632d3cac 100644 --- a/docs-site/src/content/docs/ja/reference/adapters.md +++ b/docs-site/src/content/docs/ja/reference/adapters.md @@ -200,17 +200,10 @@ Kiro には `minimal` の段階がありません。`claude-opus-5` と `claude- どのサーフェスでも同じ応答になります。拒否は上流へ送る本文を組み立てる途中で起きるため、ストリームは 開かれず上流呼び出しも発生しません。 -`ultra` には区別が必要です。呼び出し元が送った `ultra` は、Responses と Messages のどちらの経路でも -アダプターがリクエストを組み立てる前に `max` へ下げます。設定した pin は違います。 -`pinnedReasoningEffort`、`modelPinnedReasoningEfforts`、`modelPinnedEfforts` は `ultra` を受け付け、 -リクエストを解析した後に適用するため、アダプターまで届きます。ネイティブ Kiro effort を持つすべての -モデル、つまり上記の Claude id と GPT モデルでは、この pin が Kiro の受け付けない段階であるため、 -何も送信する前に 400 `invalid_request_error` でリクエストが失敗します。この拒否も `minimal` と -同じくサーフェスごとに分かれます。Responses の本文はモデルと effort を示し、Messages と Chat -Completions の本文はプロキシの固定文言だけを載せ、どちらも示しません。エミュレーションを使う -モデルでは pin は黙って捨てられ、推論シグナルのないままリクエストが送られます。これらの Claude id -のいずれかをネイティブ effort に上げた後では、以前は捨てられていた `ultra` の pin が失敗します。 -`max` を pin してください。 +呼び出し元の `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 の選択肢だけではネイティブ対応を意味しません。 diff --git a/docs-site/src/content/docs/ko/reference/adapters.md b/docs-site/src/content/docs/ko/reference/adapters.md index 30ed11451e7..56506f571f5 100644 --- a/docs-site/src/content/docs/ko/reference/adapters.md +++ b/docs-site/src/content/docs/ko/reference/adapters.md @@ -232,16 +232,10 @@ Kiro에는 `minimal` 단계가 없습니다. `claude-opus-5`와 `claude-opus-5.5 비스트리밍은 모든 표면에서 같은 응답을 냅니다. 거부는 업스트림 요청 본문을 만드는 도중에 일어나므로 스트림이 열리지 않고 업스트림 호출도 없습니다. -`ultra`는 구분이 필요합니다. 호출자가 보낸 `ultra`는 Responses와 Messages 경로 모두 어댑터가 -요청을 만들기 전에 `max`로 낮춥니다. 설정한 pin은 다릅니다. `pinnedReasoningEffort`, -`modelPinnedReasoningEfforts`, `modelPinnedEfforts`는 `ultra`를 받아들이고 요청을 파싱한 뒤에 -적용하므로 어댑터까지 도달합니다. 네이티브 Kiro effort를 쓰는 모든 모델, 즉 위에 나온 Claude id와 -GPT 모델에서는 이 pin이 Kiro가 받지 않는 단계라서 아무것도 보내기 전에 400 -`invalid_request_error`로 요청이 실패합니다. 이 거부도 `minimal`과 똑같이 표면별로 나뉩니다. -Responses 본문은 모델과 effort를 밝히고, Messages와 Chat Completions 본문은 프록시의 고정 문구만 -담으며 둘 다 밝히지 않습니다. 에뮬레이션을 쓰는 모델에서는 pin이 조용히 버려지고 추론 신호 없이 -요청이 나갑니다. 이 Claude id 중 하나를 네이티브 effort로 올린 뒤라면, 전에는 버려지던 `ultra` -pin이 이제 실패합니다. `max`를 pin하십시오. +호출자가 보낸 `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에서 에뮬레이션을 씁니다. 조절 항목이 있다고 네이티브 지원을 뜻하지는 않습니다. diff --git a/docs-site/src/content/docs/reference/adapters.md b/docs-site/src/content/docs/reference/adapters.md index 4c2bb412bb9..825d9046ec7 100644 --- a/docs-site/src/content/docs/reference/adapters.md +++ b/docs-site/src/content/docs/reference/adapters.md @@ -760,18 +760,11 @@ adapter detail with their own sentence rather than relaying it. Only Responses n 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. -`ultra` needs a distinction. A caller's own `ultra` is reduced to `max` before the adapter builds -a request, on both the Responses and Messages paths. A configured pin is not: `pinnedReasoningEffort`, -`modelPinnedReasoningEfforts` and `modelPinnedEfforts` accept `ultra` and are applied after the -request is parsed, so the adapter does receive it. For every model with native Kiro effort — the -Claude ids above and the GPT models — that pin fails the request with a 400 -`invalid_request_error` before anything is sent, because `ultra` is not a rung Kiro accepts. That -refusal is split across the surfaces exactly as `minimal` is: the Responses body names the model -and the effort, while the Messages and Chat Completions bodies carry only the fixed condition copy -and name neither. -For a model that uses emulated reasoning the pin is dropped instead, and the request goes out with -no reasoning signal at all. If you upgraded onto native effort for one of these Claude ids, a pin of -`ultra` that used to be dropped now fails: pin `max` instead. +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 diff --git a/docs-site/src/content/docs/ru/reference/adapters.md b/docs-site/src/content/docs/ru/reference/adapters.md index ebb3fe39b56..b2deb82426b 100644 --- a/docs-site/src/content/docs/ru/reference/adapters.md +++ b/docs-site/src/content/docs/ru/reference/adapters.md @@ -237,17 +237,10 @@ effort. Эти две модели Opus 5, а также модели GPT с н Потоковые и непоточные запросы читаются одинаково на всех поверхностях: отказ возникает при сборке тела запроса к провайдеру, поэтому поток не открывается и вызов провайдера не происходит. -`ultra` требует оговорки. Присланный вызывающей стороной `ultra` сводится к `max` до того, как -адаптер построит запрос, — и на пути Responses, и на пути Messages. Настроенный pin ведёт себя -иначе: `pinnedReasoningEffort`, `modelPinnedReasoningEfforts` и `modelPinnedEfforts` принимают -`ultra` и применяются уже после разбора запроса, поэтому адаптер его получает. Для каждой модели с -нативным Kiro effort — перечисленных выше идентификаторов Claude и моделей GPT — такой pin -завершает запрос ошибкой 400 `invalid_request_error` до отправки чего-либо, поскольку `ultra` — -не тот уровень, который принимает Kiro. Этот отказ делится между поверхностями так же, как -`minimal`: тело Responses называет модель и уровень, тела Messages и Chat Completions несут только -фиксированный текст прокси и не называют ни того, ни другого. Для модели на эмуляции pin просто отбрасывается, и запрос уходит вообще без сигнала -рассуждений. Если вы перевели один из этих идентификаторов Claude на нативный effort, pin `ultra`, -который раньше отбрасывался, теперь завершается ошибкой: закрепляйте `max`. +Значение `ultra` от клиента при разборе заменяется на `max`. Настройки `pinnedReasoningEffort`, +`modelPinnedReasoningEfforts` и `modelPinnedEfforts` применяются позже: для моделей с нативным +Kiro effort адаптер также заменяет закреплённый `ultra` на `max`. Для моделей с эмуляцией такой +pin отбрасывается, и запрос отправляется без сигнала рассуждений. `claude-haiku-4.5`, `claude-opus-4.5`, `claude-sonnet-4.5` и любая не перечисленная выше модель Kiro используют эмуляцию на каждом уровне; наличие настройки effort не доказывает нативную diff --git a/docs-site/src/content/docs/tr/reference/adapters.md b/docs-site/src/content/docs/tr/reference/adapters.md index 1b4a1725c28..11b870bf3f2 100644 --- a/docs-site/src/content/docs/tr/reference/adapters.md +++ b/docs-site/src/content/docs/tr/reference/adapters.md @@ -325,17 +325,10 @@ aktarmak yerine kendi metniyle değiştirir. Modeli ve seviyeyi yalnızca Respon 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. -`ultra` için bir ayrım gerekir. Çağıranın kendi gönderdiği `ultra`, hem Responses hem Messages -yolunda adaptör isteği kurmadan önce `max` seviyesine indirilir. Yapılandırılmış pin ise böyle -değildir: `pinnedReasoningEffort`, `modelPinnedReasoningEfforts` ve `modelPinnedEfforts` `ultra` -değerini kabul eder ve istek ayrıştırıldıktan sonra uygulanır, dolayısıyla adaptör onu gerçekten -alır. Yerel Kiro çabası kullanan her modelde, yani yukarıdaki Claude kimliklerinde ve GPT -modellerinde, bu pin `ultra` Kiro'nun kabul ettiği bir seviye olmadığı için isteği hiçbir şey -gönderilmeden önce 400 `invalid_request_error` ile başarısız kılar. Bu ret de `minimal` gibi yüzeylere -ayrılır: Responses gövdesi modeli ve seviyeyi belirtir, Messages ve Chat Completions gövdeleri yalnızca -proxy'nin sabit metnini taşır ve ikisini de belirtmez. Öykünme kullanan bir modelde ise pin sessizce düşürülür ve istek hiçbir akıl -yürütme sinyali olmadan gider. Bu Claude kimliklerinden birini yerel çabaya yükselttiyseniz, önceden -düşürülen bir `ultra` pin artık başarısız olur: `max` pinleyin. +Ç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 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 812ae9e4de3..1f169c51e33 100644 --- a/docs-site/src/content/docs/zh-cn/reference/adapters.md +++ b/docs-site/src/content/docs/zh-cn/reference/adapters.md @@ -205,14 +205,9 @@ Kiro 没有 `minimal` 档位。除 `claude-opus-5` 和 `claude-opus-5.5` 外的 只有 Responses 会标明模型和 effort。流式与非流式在所有接口上返回相同内容:拒绝发生在构建上游 请求体时,因此没有打开流,也没有发出上游调用。 -`ultra` 需要区分。调用方自己发送的 `ultra` 会在适配器构建请求前被降为 `max`,Responses 和 -Messages 路径都是如此。配置的 pin 则不同:`pinnedReasoningEffort`、`modelPinnedReasoningEfforts` -和 `modelPinnedEfforts` 接受 `ultra`,并在请求解析之后才应用,因此会到达适配器。对所有使用原生 -Kiro effort 的模型(即上面的 Claude id 与 GPT 模型),这个 pin 都不是 Kiro 接受的档位,请求会在 -发送任何内容之前以 400 `invalid_request_error` 失败。这次拒绝也像 `minimal` 一样按接口区分: -Responses 正文标明模型和 effort,Messages 与 Chat Completions 正文只带代理的固定文案,两者都不标明。对使用模拟推理的模型,pin 会被 -静默丢弃,请求在没有推理信号的情况下发出。如果你已把其中某个 Claude id 升级为原生 effort, -那么以前会被丢弃的 `ultra` pin 现在会失败;请改用 `max`。 +调用方的 `ultra` 在解析时转换为 `max`。`pinnedReasoningEffort`、`modelPinnedReasoningEfforts` +和 `modelPinnedEfforts` 固定值在解析后应用;对于使用原生 Kiro effort 的模型,适配器也会将固定的 +`ultra` 转为 `max`。对于模拟推理模型,该固定值会被丢弃,请求不带推理信号发送。 `claude-haiku-4.5`、`claude-opus-4.5`、`claude-sonnet-4.5` 以及上面未列出的 Kiro 模型在每个 effort 上都使用模拟推理;提供 effort 选项并不代表原生支持。 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 9ed30c95eb9..eb1a59f8ac5 100644 --- a/docs-site/src/content/docs/zh-tw/reference/adapters.md +++ b/docs-site/src/content/docs/zh-tw/reference/adapters.md @@ -185,14 +185,9 @@ Kiro 沒有 `minimal` 檔位。除 `claude-opus-5` 和 `claude-opus-5.5` 外的 只有 Responses 會標明模型與 effort。串流與非串流在所有介面上回傳相同內容:拒絕發生在建構上游 請求體時,因此沒有開啟串流,也沒有發出上游呼叫。 -`ultra` 需要區分。呼叫方自己送出的 `ultra` 會在適配器建置請求前被降為 `max`,Responses 和 -Messages 路徑都是如此。設定的 pin 則不同:`pinnedReasoningEffort`、`modelPinnedReasoningEfforts` -和 `modelPinnedEfforts` 接受 `ultra`,並在請求解析之後才套用,因此會到達適配器。對所有使用原生 -Kiro effort 的模型(即上面的 Claude id 與 GPT 模型),這個 pin 都不是 Kiro 接受的檔位,請求會在 -傳送任何內容之前以 400 `invalid_request_error` 失敗。這次拒絕也像 `minimal` 一樣按介面區分: -Responses 正文標明模型與 effort,Messages 與 Chat Completions 正文只帶代理的固定文案,兩者都不標明。 -對使用模擬推理的模型,pin 會被靜默丟棄,請求在沒有推理訊號的情況下送出。如果你已把其中某個 -Claude id 升級為原生 effort,那麼以前會被丟棄的 `ultra` pin 現在會失敗;請改用 `max`。 +呼叫方的 `ultra` 在解析時轉為 `max`。`pinnedReasoningEffort`、`modelPinnedReasoningEfforts` +和 `modelPinnedEfforts` 固定值在解析後套用;對使用原生 Kiro effort 的模型,適配器也會將固定的 +`ultra` 轉為 `max`。對使用模擬推理的模型,該固定值會被丟棄,請求不帶推理訊號送出。 `claude-haiku-4.5`、`claude-opus-4.5`、`claude-sonnet-4.5` 以及上面未列出的 Kiro 模型在每個 effort 上都使用模擬推理;提供 effort 選項不代表原生支援。 diff --git a/src/adapters/kiro/payload.ts b/src/adapters/kiro/payload.ts index 382071dac78..937127f359e 100644 --- a/src/adapters/kiro/payload.ts +++ b/src/adapters/kiro/payload.ts @@ -485,8 +485,9 @@ 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) && !kiroEmulatesUnsupportedEffort(parsed.modelId, effort)) { throw new Error(`Kiro ${normalizeKiroModelId(parsed.modelId)} does not support reasoning effort ${JSON.stringify(effort)}`); diff --git a/src/adapters/kiro/reasoning.ts b/src/adapters/kiro/reasoning.ts index 025cea2230a..f38c8e238ce 100644 --- a/src/adapters/kiro/reasoning.ts +++ b/src/adapters/kiro/reasoning.ts @@ -59,18 +59,9 @@ const KIRO_NO_XHIGH_NATIVE_MODELS = new Set(["claude-opus-4.6", "claude-sonnet-4 // value check is what keeps it that way: an effort this file has never seen still throws instead // of silently dropping reasoning. // -// `ultra` is a separate case, and the inbound fold is not the whole story. Caller-supplied -// `ultra` is folded to `max` before the adapter — the Responses parser -// (`src/responses/parser.ts`) does it for both surfaces, because the Messages translator -// (`src/claude/inbound.ts`) copies `output_config.effort` through verbatim and then hands the -// translated body to that same parser — but an operator pin is applied later, in -// `applyPinnedEffort` (`src/server/effort-policy.ts`), which runs after parsing and accepts -// `ultra` because it is a declared effort. So a `pinnedReasoningEffort: "ultra"` — or a -// `modelPinnedReasoningEfforts` / `modelPinnedEfforts` value — does reach this file. Every model -// in the native table then throws, -// and the request becomes HTTP 400 `invalid_request_error` before anything is sent. Models -// outside the table keep the emulated path, where `kiroThinkingBudget` has no `ultra` entry, so -// the pin is dropped and no reasoning signal goes out at all. +// Caller-supplied `ultra` is folded by the Responses parser, but operator pins are applied +// afterward. The Kiro payload builder folds `ultra` to `max` for native models; emulated +// models retain their existing path, which has no `ultra` thinking budget. const KIRO_EMULATED_EXTRA_EFFORT_MODELS = new Set([ "claude-opus-4.8", "claude-opus-4.7", diff --git a/structure/providers/kiro.md b/structure/providers/kiro.md index d62317dd121..b691bcbbb87 100644 --- a/structure/providers/kiro.md +++ b/structure/providers/kiro.md @@ -378,33 +378,12 @@ they keep the emulated block for `minimal` instead of failing a request they use `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 not a caller request, and it is not folded. `applyPinnedEffort` -(`src/server/effort-policy.ts`) runs after the body is parsed and writes the configured value over -the folded one; it accepts `ultra` because `pinnedReasoningEffort` and both pinned maps validate -against `isDeclaredReasoningEffort`, which includes the whole Codex ladder. So a pin of `ultra` -does reach `buildKiroPayload`, and what happens there depends on the model: - -- A model in `KIRO_NATIVE_EFFORT_FIELDS` throws, because `ultra` is outside `KIRO_NATIVE_EFFORTS` - and `kiroEmulatesUnsupportedEffort` only ever admits `minimal`. The throw becomes HTTP 400 - `invalid_request_error` on every surface, and nothing is sent upstream. What the client reads - differs: on Responses the adapter's `buildKiroPayload` message survives into `error.message` as - `Kiro does not support reasoning effort "ultra"` - (`src/server/responses/adapter-dispatch.ts`), while Messages and Chat Completions replace it with - the fixed `request_rejected` copy `The provider rejected the request.` - (`upstreamConditionMessage` in `src/lib/upstream-condition.ts`, reached through - `ownedAnthropicErrorPayload` in `src/lib/messages-error-body.ts` and through `ownedChatErrorPayload` - in `src/lib/client-error-body.ts`) because those paths re-shape the error and do not forward the - adapter detail. -- A model outside the table keeps the emulated path. `kiroThinkingBudget` has no `ultra` entry, so - the pin is silently dropped and the request goes out with no reasoning signal — neither the - native field nor the thinking block. - -This is a real upgrade effect for the seven Claude ids above, not an accident of this table. On a -base that predates them they were outside the native table, so a pinned `ultra` was dropped the -same way `claude-haiku-4.5`, `claude-opus-4.5`, `claude-sonnet-4.5` and the non-Claude emulated -models still are. Becoming native on this branch turned that silent drop into a 400. The GPT -models and Opus 5 / Opus 5.5 already rejected a pinned `ultra` on that base, so nothing changed -for them. `tests/providers/kiro/kiro-pinned-effort-ultra.test.ts` holds both halves of the split. +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 diff --git a/tests/providers/kiro/kiro-pinned-effort-ultra.test.ts b/tests/providers/kiro/kiro-pinned-effort-ultra.test.ts index fb6b7684ccf..2917a1359cb 100644 --- a/tests/providers/kiro/kiro-pinned-effort-ultra.test.ts +++ b/tests/providers/kiro/kiro-pinned-effort-ultra.test.ts @@ -2,7 +2,6 @@ import { afterEach, beforeEach, expect, test } from "bun:test"; import { mkdtempSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { KIRO_NATIVE_EFFORT_FIELDS } from "../../../src/adapters/kiro/reasoning"; import { getAccountSet, saveCredential, setActiveAccount } from "../../../src/oauth/store"; import { handleChatCompletions } from "../../../src/server/chat-completions"; import { handleClaudeMessages } from "../../../src/server/claude-messages"; @@ -41,11 +40,17 @@ afterEach(() => { }); /** A provider pin is an operator setting; `ultra` is a declared effort, so config admits it. */ -function config(models: string[], pin?: string): OcxConfig { - return { port: 0, defaultProvider: "kiro", providers: { kiro: { - adapter: "kiro", authMode: "oauth", baseUrl: "https://runtime.us-east-1.kiro.dev", - models, ...(pin ? { pinnedReasoningEffort: pin } : {}), - } } } as unknown as OcxConfig; +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. */ @@ -59,7 +64,7 @@ function captureWire(): { bodies: Record[] } { } async function responsesRequest( - model: string, effort: string | undefined, pin: string | undefined, stream = false, + model: string, effort: string | undefined, pin: Pin | undefined, stream = false, ) { const captured = captureWire(); const response = await handleResponses(new Request("http://localhost/v1/responses", { @@ -83,7 +88,7 @@ async function responsesRequest( * 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: string | undefined, stream = false) { +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", @@ -101,7 +106,7 @@ async function messagesRequest(model: string, pin: string | undefined, stream = } /** Chat Completions is the third surface that reaches this adapter, and its copy is fixed too. */ -async function chatRequest(model: string, pin: string | undefined, stream = false) { +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" }, @@ -122,70 +127,45 @@ const userContent = (wire: Record | undefined) => (wire?.conver currentMessage: { userInputMessage: { content: string } }; } | undefined)?.currentMessage.userInputMessage.content; -// The claim this file replaces: "ultra never reaches the adapter". It reaches the adapter whenever -// an operator pin supplies it, because pins are applied AFTER the inbound fold — the Responses -// parser turns a caller's `ultra` into `max` (src/responses/parser.ts) and the Messages translator -// copies a caller's `output_config.effort` through (src/claude/inbound.ts), but -// `applyPinnedEffort` (src/server/effort-policy.ts) writes the configured value last. -test("an operator pin of ultra reaches the adapter, which admits only its own ladder", async () => { - const native = await responsesRequest("claude-opus-4.8", "high", "ultra"); - expect(native.status).toBe(400); - expect(native.error).toEqual({ - message: 'Kiro claude-opus-4.8 does not support reasoning effort "ultra"', - type: "invalid_request_error", code: "invalid_request_error", - }); - // Nothing was sent: the refusal happens while the payload is built, not after an upstream round trip. - expect(native.wires).toHaveLength(0); - - // The same request without the pin is folded to `max` and does reach the wire. - const folded = await responsesRequest("claude-opus-4.8", "ultra", undefined); - expect(folded.status).toBe(500); - expect(folded.wires).toHaveLength(1); - expect(reasoningField(folded.wires[0])).toEqual({ output_config: { effort: "max" } }); -}); - -test("every model with a native Kiro effort field rejects a pinned ultra instead of sending it", async () => { - const models = Object.keys(KIRO_NATIVE_EFFORT_FIELDS); - // Guard the enumeration itself: an empty table would make the loop below pass vacuously. - expect(models.length).toBeGreaterThan(0); - for (const model of models) { - for (const stream of [false, true]) { - const { status, error, wires, contentType } = await responsesRequest(model, undefined, "ultra", stream); - expect({ model, stream, status }).toEqual({ model, stream, status: 400 }); - // The Responses public contract carries the adapter's sentence verbatim, stream or not. - expect({ model, stream, error }).toEqual({ - model, stream, - error: { - message: `Kiro ${model} does not support reasoning effort "ultra"`, - type: "invalid_request_error", code: "invalid_request_error", - }, +// 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" } }); + } }); - expect({ model, stream, sends: wires.length }).toEqual({ model, stream, sends: 0 }); - // A refusal raised before the stream opens is JSON on both request shapes, never SSE. - expect(contentType).toContain("application/json"); } } +} + +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 answers the same refusals with its own fixed copy", async () => { - // The OpenAI-compatible body adds `param`/`code` around the same fixed sentence. That surface is - // asserted separately so a Messages-only change cannot silently leave it forwarding the detail. - for (const [model, pin] of [["gpt-5.6-sol", "ultra"], ["claude-opus-5.5", "minimal"]] as const) { - for (const stream of [false, true]) { - const { status, body, wires, contentType } = await chatRequest(model, pin, stream); - expect({ model, pin, stream, status }).toEqual({ model, pin, stream, status: 400 }); - expect({ model, pin, stream, body }).toEqual({ - model, pin, stream, - body: { - error: { - message: "The provider rejected the request.", type: "invalid_request_error", - param: null, code: "invalid_request_error", - }, - }, - }); - expect({ model, pin, stream, sends: wires.length }).toEqual({ model, pin, stream, sends: 0 }); - expect(contentType).toContain("application/json"); - } +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"); } }); @@ -229,7 +209,7 @@ test("the Messages surface replaces the adapter detail with the fixed condition // 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-4.8", "ultra"], ["claude-opus-5", "minimal"]] as const) { + 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 }); diff --git a/tests/providers/kiro/kiro-tool-catalog.test.ts b/tests/providers/kiro/kiro-tool-catalog.test.ts index b726d6ff6e2..e239307ff49 100644 --- a/tests/providers/kiro/kiro-tool-catalog.test.ts +++ b/tests/providers/kiro/kiro-tool-catalog.test.ts @@ -254,7 +254,7 @@ describe("kiro tool schemas by model family", () => { describe("Kiro effort fallback", () => { test("the minimal fallback is that one value, not any effort the ladder does not know", async () => { - // The reachable outside-ladder value is `minimal`; `ultra` is folded to `max` upstream. If the + // 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"]) { From 3861572abc8ac7411df4558753c323fb65f86e79 Mon Sep 17 00:00:00 2001 From: JUN Date: Fri, 13 Nov 2026 11:00:30 +0900 Subject: [PATCH 17/17] docs(kiro): clarify minimal refusal details Co-authored-by: svc-aibot-363 <271381517+svc-aibot-363@users.noreply.github.com> --- structure/providers/kiro.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/structure/providers/kiro.md b/structure/providers/kiro.md index b691bcbbb87..079b4d94b2c 100644 --- a/structure/providers/kiro.md +++ b/structure/providers/kiro.md @@ -365,9 +365,9 @@ 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. The surface split -described below for a pinned `ultra` applies here unchanged, because it is the same throw on the -same path. `minimal` is the only outside-ladder value a caller's own request can deliver: the +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