fix(ai): improved reasoning fallback logic for equidistant tiers

- Update fallback selection to prefer higher effort tiers when distance is equal.
- Add test case verifying fallback from unsupported "medium" effort to "high".
This commit is contained in:
can1357
2026-06-19 14:49:05 +02:00
parent 66872fbac3
commit f53daec09f
2 changed files with 40 additions and 1 deletions
@@ -230,14 +230,16 @@ function nearestEnabledReasoningFallback(currentEffort: string, allowed: Set<str
if (currentRank === undefined) return undefined;
let best: string | undefined;
let bestDistance = Number.POSITIVE_INFINITY;
let bestRank = Number.NEGATIVE_INFINITY;
for (const candidate of allowedEnabled) {
if (candidate === current) continue;
const candidateRank = REASONING_VALUE_RANK[candidate];
if (candidateRank === undefined) continue;
const distance = Math.abs(candidateRank - currentRank);
if (distance < bestDistance) {
if (distance < bestDistance || (distance === bestDistance && candidateRank > bestRank)) {
best = candidate;
bestDistance = distance;
bestRank = candidateRank;
}
}
return best;
@@ -71,6 +71,19 @@ function invalidReasoningResponse(param: "reasoning_effort" | "reasoning.effort"
{ status: 400, headers: { "content-type": "application/json" } },
);
}
function invalidMediumReasoningResponse(): Response {
return new Response(
JSON.stringify({
error: {
message: 'reasoning.effort: Invalid option: expected one of "high"|"low"|"minimal"|"none"',
type: "invalid_request_error",
param: "reasoning.effort",
},
}),
{ status: 400, headers: { "content-type": "application/json" } },
);
}
function pipeDelimitedReasoningEffortResponse(): Response {
return new Response(
JSON.stringify({
@@ -271,6 +284,30 @@ describe("OpenAI reasoning effort fallback retry", () => {
expect(bodies.map(body => (body.reasoning as { effort?: string } | undefined)?.effort)).toEqual(["max", "xhigh"]);
});
it("retries medium as high when medium is missing and high is the closest upper tier", async () => {
const bodies: Record<string, unknown>[] = [];
const fetchMock: FetchImpl = Object.assign(
async (_input: string | URL | Request, init?: RequestInit): Promise<Response> => {
const body = parseJsonBody(init);
bodies.push(body);
return bodies.length === 1 ? invalidMediumReasoningResponse() : createResponsesSseResponse();
},
{ preconnect: fetch.preconnect },
);
const result = await streamOpenAIResponses(createResponsesModel(), testContext, {
apiKey: "test-key",
fetch: fetchMock,
reasoning: "medium",
}).result();
expect(result.stopReason).toBe("stop");
expect(bodies.map(body => (body.reasoning as { effort?: string } | undefined)?.effort)).toEqual([
"medium",
"high",
]);
});
it("retries Azure Responses xhigh as provider max", async () => {
const bodies: Record<string, unknown>[] = [];
const fetchMock: FetchImpl = Object.assign(