fix(kiro): preserve GPT-5.6 Max reasoning via Responses (#9163)

Validated in local merge-train (devbox-vm-06-dev002) @ combined-tip (FAST gates green: static + changed tests + vitest — only pre-existing audit.test.ts flake). Evidence: /home/diegosouzapw/dev/proxys/OmniRoute/.claude/worktrees/merge-train-20260805-213228-suite.log
This commit is contained in:
VXNCXNX
2026-08-06 02:44:02 +02:00
committed by GitHub
parent 6c0437f136
commit a5a78fd1d8
10 changed files with 150 additions and 38 deletions

View File

@@ -3,7 +3,15 @@
* Delegates to the canonical translator to avoid logic duplication.
*/
import { openaiResponsesToOpenAIRequest } from "../request/openai-responses.ts";
import { toRecord } from "../request/openai-responses/helpers.ts";
export function convertResponsesApiFormat(body, credentials = null, provider = null) {
return openaiResponsesToOpenAIRequest(provider, body, null, credentials);
const bodyModel = toRecord(body).model;
const requestedModel =
typeof bodyModel === "string" && bodyModel.trim().length > 0
? bodyModel.includes("/") || typeof provider !== "string" || provider.length === 0
? bodyModel
: `${provider}/${bodyModel}`
: provider;
return openaiResponsesToOpenAIRequest(requestedModel, body, null, credentials);
}

View File

@@ -724,7 +724,7 @@ export function openaiResponsesToOpenAIRequest(
const reasoningRec = toRecord(root.reasoning);
const effort = toString(reasoningRec.effort);
if (effort && result.reasoning_effort === undefined) {
result.reasoning_effort = normalizeResponsesReasoningEffort(effort);
result.reasoning_effort = normalizeResponsesReasoningEffort(effort, model);
}
if (
credentialRecord._copilotClient === true &&

View File

@@ -52,13 +52,18 @@ export function imageUrlToText(value: unknown): string {
const CODEX_GPT_5_6_MODEL_PATTERN =
/^gpt-5\.6-(?:sol|terra|luna)(?:-(?:none|low|medium|high|xhigh|max|ultra))?$/;
const KIRO_GPT_5_6_MODEL_PATTERN =
/^(?:kiro|kr)\/gpt-5\.6-(?:sol|terra|luna)(?:-(?:none|low|medium|high|xhigh|max))?$/;
function supportsNativeMaxReasoningEffort(model: unknown): boolean {
const normalizedModel = toString(model)
.trim()
.toLowerCase()
.replace(/^(?:codex|cx)\//, "");
return CODEX_GPT_5_6_MODEL_PATTERN.test(normalizedModel);
return (
CODEX_GPT_5_6_MODEL_PATTERN.test(normalizedModel) ||
KIRO_GPT_5_6_MODEL_PATTERN.test(toString(model).trim().toLowerCase())
);
}
export function normalizeResponsesReasoningEffort(value: unknown, model?: unknown): string {

View File

@@ -14,6 +14,7 @@ import {
import {
resolveKiroModelAlias,
supportsKiroAdaptiveThinking,
supportsKiroNativeReasoning,
} from "./openai-to-kiro/adaptiveThinking.ts";
/**
@@ -786,6 +787,7 @@ export function buildKiroPayload(model, body, stream, credentials) {
topP?: number;
};
additionalModelRequestFields?: {
reasoning?: { effort: string };
thinking?: { type: string; display?: string };
output_config?: { effort: string };
max_tokens?: number;
@@ -870,29 +872,43 @@ export function buildKiroPayload(model, body, stream, credentials) {
// thinking:{type:"adaptive"} + a clamped max_tokens), forwarded to AWS by
// the Kiro executor's transformRequest allowlist — the graded effort lever,
// gated on Kiro's adaptive-thinking allowlist (#6576), not supportsReasoning().
// GPT-5.6 models use the native `reasoning:{effort}` field instead. They must
// not receive the Claude `output_config`/`thinking` envelope: Kiro rejects it
// as an unknown field for the GPT-5.6 family.
const requestedEffort = resolveKiroEffort(body) || (modelRequestsThinking ? "high" : "");
const kiroEffort = supportsKiroAdaptiveThinking(normalizedModel) ? requestedEffort : "";
const usesNativeReasoning = supportsKiroNativeReasoning(normalizedModel);
const usesAdaptiveThinking = supportsKiroAdaptiveThinking(normalizedModel);
const kiroEffort = usesNativeReasoning || usesAdaptiveThinking ? requestedEffort : "";
if (kiroEffort) {
// `<thinking_mode>` / `<max_thinking_length>` are Kiro/CodeWhisperer prompt
// conventions (NOT Anthropic API params); the length is a soft hint (the hard
// enable signal is `<thinking_mode>`), clamped to the model's thinking cap.
const thinkingLength = capThinkingBudget(normalizedModel, thinkingLengthForEffort(kiroEffort));
const directive =
`<thinking_mode>enabled</thinking_mode>` +
`<max_thinking_length>${thinkingLength}</max_thinking_length>`;
payload.conversationState.currentMessage.userInputMessage.content = `${directive}\n\n${payload.conversationState.currentMessage.userInputMessage.content}`;
const fields: {
output_config: { effort: string };
thinking: { type: string; display: string };
reasoning?: { effort: string };
output_config?: { effort: string };
thinking?: { type: string; display: string };
max_tokens?: number;
} = {
output_config: { effort: kiroEffort },
thinking: { type: "adaptive", display: "summarized" },
};
} = usesNativeReasoning
? { reasoning: { effort: kiroEffort } }
: {
output_config: { effort: kiroEffort },
thinking: { type: "adaptive", display: "summarized" },
};
if (usesAdaptiveThinking) {
// `<thinking_mode>` / `<max_thinking_length>` are Kiro/CodeWhisperer prompt
// conventions (NOT Anthropic API params); the length is a soft hint (the hard
// enable signal is `<thinking_mode>`), clamped to the model's thinking cap.
const thinkingLength = capThinkingBudget(
normalizedModel,
thinkingLengthForEffort(kiroEffort)
);
const directive =
`<thinking_mode>enabled</thinking_mode>` +
`<max_thinking_length>${thinkingLength}</max_thinking_length>`;
payload.conversationState.currentMessage.userInputMessage.content = `${directive}\n\n${payload.conversationState.currentMessage.userInputMessage.content}`;
}
// Forward max_tokens only when the client set one, clamped to the model's
// output window (floor 1024) — matches pi-kiro and avoids an over-budget reject.
if (maxTokens > 0) {
if (usesAdaptiveThinking && maxTokens > 0) {
const capped = capMaxOutputTokens(normalizedModel, maxTokens) ?? maxTokens;
fields.max_tokens = Math.max(Math.floor(capped), 1024);
}

View File

@@ -7,17 +7,21 @@
* rejects the field for `claude-sonnet-4.5` and `claude-haiku-4.5` with a raw
* upstream 400 (`additionalModelRequestFields is not supported for this
* model`, issue #6576) even though both ARE thinking-capable on Anthropic's
* direct API. Only `claude-sonnet-5` is confirmed to accept the adaptive
* envelope on Kiro today — keep this allowlist in sync with
* `open-sse/config/providers/registry/kiro/index.ts` if Kiro's catalog or
* upstream behavior changes.
* direct API. `claude-sonnet-5` is confirmed to accept the adaptive envelope
* on Kiro today. GPT-5.6 models use Kiro's separate `reasoning.effort` shape,
* not this Claude adaptive envelope.
*/
const KIRO_ADAPTIVE_THINKING_MODELS = new Set(["claude-sonnet-5"]);
const KIRO_NATIVE_REASONING_MODELS = new Set(["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"]);
export function supportsKiroAdaptiveThinking(normalizedModel: string): boolean {
return KIRO_ADAPTIVE_THINKING_MODELS.has(normalizedModel);
}
export function supportsKiroNativeReasoning(normalizedModel: string): boolean {
return KIRO_NATIVE_REASONING_MODELS.has(normalizedModel);
}
const KIRO_UNSUPPORTED_AGENTIC_MESSAGE =
"Kiro agentic aliases are not supported. The '-agentic' suffix did not change the " +
"upstream request; select a real Kiro model instead.";

View File

@@ -12,7 +12,8 @@ import { z } from "zod";
* mappers already read.
*
* The provider-agnostic vocabulary remains five values. Provider-native additions such as
* Codex GPT-5.6 Max and Ultra are exposed separately without widening this request contract.
* Codex GPT-5.6 Max/Ultra and Kiro GPT-5.6 Max are exposed separately without widening this
* request contract.
*/
export const CANONICAL_EFFORT_VALUES = ["none", "low", "medium", "high", "xhigh"] as const;
@@ -29,16 +30,21 @@ export function extendCodexGpt56EffortValues(
const normalizedModel = model
?.trim()
.toLowerCase()
.replace(/^(?:codex|cx)\//, "");
if (!normalizedModel || (normalizedProvider !== "codex" && normalizedProvider !== "cx")) {
return values;
}
.replace(/^(?:codex|cx|kiro|kr)\//, "");
if (!normalizedModel) return values;
const match = normalizedModel.match(
/^gpt-5\.6-(sol|terra|luna)(?:-(?:none|low|medium|high|xhigh|max|ultra))?$/
);
if (!match) return values;
const isKiroProvider = normalizedProvider === "kiro" || normalizedProvider === "kr";
if (isKiroProvider) {
return values.includes("max") ? values : [...values, "max"];
}
if (normalizedProvider !== "codex" && normalizedProvider !== "cx") return values;
const nativeValues = ["low", "medium", "high", "xhigh", "max"];
return match[1] === "luna" ? nativeValues : [...nativeValues, "ultra"];
}

View File

@@ -15,6 +15,7 @@ import {
intersectStringArrays,
minKnownNumber,
maybeOmitCatalogModelName,
getThinkingCapabilityFields,
} from "../../src/app/api/v1/models/catalogHelpers.ts";
import {
qualifyOpenRouterModelId,
@@ -73,6 +74,16 @@ test("catalogHelpers: intersectStringArrays (dedup + common)", () => {
assert.deepEqual(intersectStringArrays([["a"], []]), []);
});
test("catalogHelpers: Kiro GPT-5.6 models expose the native Max tier", () => {
for (const model of ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"]) {
assert.deepEqual(getThinkingCapabilityFields("kr", model, true), {
thinking: true,
supportsThinking: true,
effort_tiers: ["none", "low", "medium", "high", "xhigh", "max"],
});
}
});
test("catalogHelpers: minKnownNumber ignores non-positive/unknown", () => {
assert.equal(minKnownNumber([3, 1, 2]), 1);
assert.equal(minKnownNumber([undefined, 0, -5, 7]), 7);

View File

@@ -9,15 +9,10 @@ import path from "node:path";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-effort-6241-"));
process.env.DATA_DIR = TEST_DATA_DIR;
const {
CANONICAL_EFFORT_VALUES,
normalizeEffort,
effortRequestSchema,
normalizeReasoningRequest,
} = await import("../../src/shared/reasoning/effortStandardization.ts");
const { providerChatCompletionSchema } = await import(
"../../src/shared/validation/schemas/apiV1.ts"
);
const { CANONICAL_EFFORT_VALUES, normalizeEffort, effortRequestSchema, normalizeReasoningRequest } =
await import("../../src/shared/reasoning/effortStandardization.ts");
const { providerChatCompletionSchema } =
await import("../../src/shared/validation/schemas/apiV1.ts");
const core = await import("../../src/lib/db/core.ts");
const modelsDevSync = await import("../../src/lib/modelsDevSync.ts");
const registry = await import("../../src/lib/modelMetadataRegistry.ts");
@@ -183,3 +178,16 @@ test("enrichCatalogModelEntry exposes supportsThinking + effort_tiers for a thin
assert.equal(caps.thinking, true);
assert.equal(caps.reasoning, true);
});
test("enrichCatalogModelEntry exposes Max for Kiro GPT-5.6 Luna", () => {
const enriched = registry.enrichCatalogModelEntry({
id: "kr/gpt-5.6-luna",
object: "model",
owned_by: "kr",
root: "gpt-5.6-luna",
}) as Record<string, unknown>;
const caps = enriched.capabilities as Record<string, unknown>;
assert.equal(caps.supportsThinking, true);
assert.deepEqual(caps.effort_tiers, ["none", "low", "medium", "high", "xhigh", "max"]);
});

View File

@@ -20,6 +20,7 @@ import {
openaiResponsesToOpenAIRequest,
} from "../../open-sse/translator/request/openai-responses.ts";
import { convertResponsesApiFormat } from "../../open-sse/translator/helpers/responsesApiHelper.ts";
import { buildKiroPayload } from "../../open-sse/translator/request/openai-to-kiro.ts";
function asRecord(value: unknown): Record<string, unknown> {
return value as Record<string, unknown>;
@@ -61,6 +62,34 @@ test("Responses -> Chat preserves reasoning.effort via the helper wrapper", () =
assert.equal(out.reasoning, undefined);
});
test("Responses -> Kiro preserves literal Max for GPT-5.6 models", () => {
for (const model of ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"]) {
const converted = asRecord(
convertResponsesApiFormat(
{
model: `kr/${model}`,
input: "hello",
reasoning: { effort: "max" },
},
null,
"kiro"
)
);
assert.equal(converted.reasoning_effort, "max");
const payload = buildKiroPayload(model, converted, false, null);
assert.equal(payload.additionalModelRequestFields?.reasoning?.effort, "max");
assert.equal(payload.additionalModelRequestFields?.output_config, undefined);
assert.equal(payload.additionalModelRequestFields?.thinking, undefined);
assert.equal(payload.additionalModelRequestFields?.max_tokens, undefined);
assert.doesNotMatch(
payload.conversationState.currentMessage.userInputMessage.content,
/<thinking_mode>/
);
}
});
test("Responses -> Chat does not overwrite an explicit reasoning_effort", () => {
const out = asRecord(
openaiResponsesToOpenAIRequest(

View File

@@ -1135,6 +1135,31 @@ test("buildKiroPayload enables thinking mode for Claude models via reasoning_eff
);
});
test("buildKiroPayload uses native Max reasoning for Kiro GPT-5.6 models", () => {
for (const model of ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"]) {
const result = buildKiroPayload(
model,
{
messages: [{ role: "user", content: "Solve a hard problem" }],
reasoning_effort: "max",
max_tokens: 64000,
},
false,
null
);
assert.ok(result.additionalModelRequestFields, "Max reasoning must be forwarded to Kiro");
assert.equal(result.additionalModelRequestFields.reasoning.effort, "max");
assert.equal(result.additionalModelRequestFields.output_config, undefined);
assert.equal(result.additionalModelRequestFields.thinking, undefined);
assert.equal(result.additionalModelRequestFields.max_tokens, undefined);
assert.doesNotMatch(
result.conversationState.currentMessage.userInputMessage.content,
/<thinking_mode>/
);
}
});
test("buildKiroPayload drops temperature when thinking is enabled", () => {
const body = {
messages: [{ role: "user", content: "Solve a hard problem" }],