mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-06 07:12:12 +03:00
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:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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.";
|
||||
|
||||
@@ -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"];
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"]);
|
||||
});
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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" }],
|
||||
|
||||
Reference in New Issue
Block a user