fix(agents): keep responses web search reasoning compatible

This commit is contained in:
Peter Steinberger
2026-04-26 19:14:55 +01:00
parent fc6cfbd418
commit f2dab9b334
3 changed files with 131 additions and 4 deletions

View File

@@ -700,6 +700,36 @@ describe("applyExtraParamsToAgent", () => {
});
});
it("keeps OpenAI Responses web_search compatible when thinking is minimal", () => {
const payload = runResponsesPayloadMutationCase({
applyProvider: "openai",
applyModelId: "gpt-5",
model: {
api: "openai-responses",
provider: "openai",
id: "gpt-5",
baseUrl: "http://127.0.0.1:19191/v1",
reasoning: true,
} as Model<"openai-responses">,
payload: {
model: "gpt-5",
input: [],
tools: [
{
type: "function",
name: "web_search",
description: "Search the web",
parameters: { type: "object", properties: {} },
},
],
reasoning: { effort: "low", summary: "auto" },
},
thinkingLevel: "minimal",
});
expect(payload.reasoning).toEqual({ effort: "low", summary: "auto" });
});
it("strips disabled reasoning payloads for proxied OpenAI responses routes", () => {
const payloads: Record<string, unknown>[] = [];
const baseStreamFn: StreamFn = (_model, _context, options) => {

View File

@@ -159,6 +159,32 @@ describe("createOpenAIThinkingLevelWrapper", () => {
}
});
it("raises minimal reasoning for web_search on loopback Responses routes", () => {
const payloads: Array<Record<string, unknown>> = [];
const baseStreamFn: StreamFn = (_model, _context, options) => {
const payload: Record<string, unknown> = {
reasoning: { effort: "minimal", summary: "auto" },
tools: [{ type: "function", name: "web_search" }],
};
options?.onPayload?.(payload, _model);
payloads.push(structuredClone(payload));
return createAssistantMessageEventStream();
};
const wrapped = createOpenAIThinkingLevelWrapper(baseStreamFn, "minimal");
void wrapped(
{
api: "openai-responses",
provider: "openai",
id: "gpt-5",
baseUrl: "http://127.0.0.1:19191/v1",
} as Model<"openai-responses">,
{ messages: [] },
{},
);
expect(payloads[0]?.reasoning).toEqual({ effort: "low", summary: "auto" });
});
it.each([
{
api: "openai-responses",

View File

@@ -9,6 +9,7 @@ import {
resolveCodexNativeSearchActivation,
} from "../codex-native-web-search.js";
import { flattenCompletionMessagesToStringContent } from "../openai-completions-string-content.js";
import { resolveOpenAIReasoningEffortForModel } from "../openai-reasoning-effort.js";
import {
applyOpenAIResponsesPayloadPolicy,
resolveOpenAIResponsesPayloadPolicy,
@@ -85,6 +86,66 @@ function shouldFlattenOpenAICompletionMessages(model: {
return model.api === "openai-completions" && compat?.requiresStringContent === true;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value && typeof value === "object" && !Array.isArray(value));
}
function hasResponsesWebSearchTool(tools: unknown): boolean {
if (!Array.isArray(tools)) {
return false;
}
return tools.some((tool) => {
if (!isRecord(tool)) {
return false;
}
if (tool.type === "web_search") {
return true;
}
if (tool.type === "function" && tool.name === "web_search") {
return true;
}
const fn = tool.function;
return isRecord(fn) && fn.name === "web_search";
});
}
function resolveOpenAIThinkingPayloadEffort(params: {
model: { provider?: unknown; id?: unknown; baseUrl?: unknown; api?: unknown; compat?: unknown };
payloadObj: Record<string, unknown>;
thinkingLevel: ThinkLevel;
}) {
const mapped = mapThinkingLevelToReasoningEffort(params.thinkingLevel);
if (mapped !== "minimal" || !hasResponsesWebSearchTool(params.payloadObj.tools)) {
return mapped;
}
return (
resolveOpenAIReasoningEffortForModel({
model: params.model,
effort: "low",
}) ?? mapped
);
}
function raiseMinimalReasoningForResponsesWebSearchPayload(params: {
model: { provider?: unknown; id?: unknown; baseUrl?: unknown; api?: unknown; compat?: unknown };
payloadObj: Record<string, unknown>;
}): void {
const reasoning = params.payloadObj.reasoning;
if (!isRecord(reasoning) || reasoning.effort !== "minimal") {
return;
}
if (!hasResponsesWebSearchTool(params.payloadObj.tools)) {
return;
}
const nextEffort = resolveOpenAIReasoningEffortForModel({
model: params.model,
effort: "low",
});
if (nextEffort && nextEffort !== "minimal" && nextEffort !== "none") {
reasoning.effort = nextEffort;
}
}
function normalizeOpenAIServiceTier(value: unknown): OpenAIServiceTier | undefined {
if (typeof value !== "string") {
return undefined;
@@ -240,7 +301,12 @@ export function createOpenAIThinkingLevelWrapper(
}
return (model, context, options) => {
if (!shouldApplyOpenAIReasoningCompatibility(model)) {
return underlying(model, context, options);
if (thinkingLevel === "off") {
return underlying(model, context, options);
}
return streamWithPayloadPatch(underlying, model, context, options, (payloadObj) => {
raiseMinimalReasoningForResponsesWebSearchPayload({ model, payloadObj });
});
}
return streamWithPayloadPatch(underlying, model, context, options, (payloadObj) => {
const existingReasoning = payloadObj.reasoning;
@@ -251,8 +317,13 @@ export function createOpenAIThinkingLevelWrapper(
return;
}
const reasoningEffort = resolveOpenAIThinkingPayloadEffort({
model,
payloadObj,
thinkingLevel,
});
if (existingReasoning === "none") {
payloadObj.reasoning = { effort: mapThinkingLevelToReasoningEffort(thinkingLevel) };
payloadObj.reasoning = { effort: reasoningEffort };
return;
}
if (
@@ -260,8 +331,8 @@ export function createOpenAIThinkingLevelWrapper(
typeof existingReasoning === "object" &&
!Array.isArray(existingReasoning)
) {
(existingReasoning as Record<string, unknown>).effort =
mapThinkingLevelToReasoningEffort(thinkingLevel);
(existingReasoning as Record<string, unknown>).effort = reasoningEffort;
raiseMinimalReasoningForResponsesWebSearchPayload({ model, payloadObj });
}
});
};