fix(ai): send reasoning effort to OpenAI models on Bedrock Converse (#10142)

buildAdditionalModelRequestFields only handled Claude, so the configured
thinking level never reached OpenAI models on Bedrock and they always ran at
Bedrock's default effort. gpt-oss now gets the flat reasoning_effort, clamped
to low/medium/high. Other GPT models (GPT-5.x, GPT-6) get nested
reasoning.effort, with minimal sent as low since Bedrock rejects it.

closes #9331
This commit is contained in:
Justin Santer 2026-10-06 12:06:46 -04:00 • committed by GitHub
parent 311f0e020d
commit 2989eb581f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 98 additions and 0 deletions

View file

@ -1308,9 +1308,45 @@ function buildAdditionalModelRequestFields(
return result;
}
const candidates = getModelMatchCandidates(model.id, model.name);
if (candidates.some((s) => s.includes("gpt-oss"))) {
return { reasoning_effort: OPENAI_GPT_OSS_EFFORT[options.reasoning] };
}
if (candidates.some((s) => s.includes("gpt-"))) {
const mapped = model.thinkingLevelMap?.[options.reasoning];
return {
reasoning: { effort: typeof mapped === "string" ? mapped : OPENAI_GPT_EFFORT[options.reasoning] },
};
}
return undefined;
}
type OpenAIGptEffort = "low" | "medium" | "high" | "xhigh" | "max";
type OpenAIGptOssEffort = "low" | "medium" | "high";
/** OpenAI GPT models (GPT-5.x, GPT-6) take a nested `reasoning.effort` and reject `minimal`. */
const OPENAI_GPT_EFFORT: Record<ThinkingLevel, OpenAIGptEffort> = {
minimal: "low",
low: "low",
medium: "medium",
high: "high",
xhigh: "xhigh",
max: "max",
};
/** gpt-oss takes a flat `reasoning_effort` and only accepts low, medium and high. */
const OPENAI_GPT_OSS_EFFORT: Record<ThinkingLevel, OpenAIGptOssEffort> = {
minimal: "low",
low: "low",
medium: "medium",
high: "high",
xhigh: "high",
max: "high",
};
function createImageBlock(mimeType: string, data: string) {
let format: ImageFormat;
switch (mimeType) {

View file

@ -14,6 +14,8 @@ interface BedrockThinkingPayload {
};
output_config?: { effort?: string };
anthropic_beta?: string[];
reasoning?: { effort?: string };
reasoning_effort?: string;
};
}
@ -197,6 +199,66 @@ describe("Bedrock thinking payload", () => {
});
});
describe("Bedrock OpenAI reasoning payload", () => {
// Regression for #9331: the configured thinking level never reached OpenAI models on Bedrock.
it.each([
["minimal", "low"],
["low", "low"],
["medium", "medium"],
["high", "high"],
["xhigh", "xhigh"],
["max", "max"],
] as const)("sends reasoning=%s as reasoning.effort=%s for GPT-6 and GPT-5.6", async (reasoning, effort) => {
for (const id of ["global.openai.gpt-6-sol", "us.openai.gpt-6-luna", "global.openai.gpt-5.6-sol"] as const) {
const payload = await capturePayload(getModel("amazon-bedrock", id), { reasoning });
expect(payload.additionalModelRequestFields, id).toEqual({ reasoning: { effort } });
}
});
it("sends reasoning.effort when only model.name identifies a GPT model", async () => {
const model: Model<"bedrock-converse-stream"> = {
...getModel("amazon-bedrock", "global.openai.gpt-6-sol"),
id: "arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/my-profile",
name: "GPT-6 Sol",
};
const payload = await capturePayload(model, { reasoning: "medium" });
expect(payload.additionalModelRequestFields).toEqual({ reasoning: { effort: "medium" } });
});
it("sends flat reasoning_effort for gpt-oss, clamped to high", async () => {
const model = getModel("amazon-bedrock", "openai.gpt-oss-120b-1:0");
expect((await capturePayload(model, { reasoning: "minimal" })).additionalModelRequestFields).toEqual({
reasoning_effort: "low",
});
expect((await capturePayload(model, { reasoning: "medium" })).additionalModelRequestFields).toEqual({
reasoning_effort: "medium",
});
expect((await capturePayload(model, { reasoning: "xhigh" })).additionalModelRequestFields).toEqual({
reasoning_effort: "high",
});
});
it("sends no reasoning fields when reasoning is off", async () => {
let captured: BedrockThinkingPayload | undefined;
const s = streamBedrock(getModel("amazon-bedrock", "global.openai.gpt-6-sol"), normalizeContext(makeContext()), {
onPayload: (payload) => {
captured = payload as BedrockThinkingPayload;
throw new PayloadCaptured();
},
});
for await (const event of s) {
if (event.type === "error") break;
}
expect(captured).toBeDefined();
expect(captured?.additionalModelRequestFields).toBeUndefined();
});
});
describe.skipIf(!hasBedrockCredentials())("Bedrock Claude max tokens E2E", () => {
it(
"uses the model maxTokens cap instead of Bedrock's 4096-token default for adaptive Claude models",