diff --git a/src/agents/pi-embedded-runner-extraparams.test.ts b/src/agents/pi-embedded-runner-extraparams.test.ts index 120559d6f6a..044738e0a5c 100644 --- a/src/agents/pi-embedded-runner-extraparams.test.ts +++ b/src/agents/pi-embedded-runner-extraparams.test.ts @@ -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[] = []; const baseStreamFn: StreamFn = (_model, _context, options) => { diff --git a/src/agents/pi-embedded-runner/openai-stream-wrappers.test.ts b/src/agents/pi-embedded-runner/openai-stream-wrappers.test.ts index 663727a8922..9bacaa51d9d 100644 --- a/src/agents/pi-embedded-runner/openai-stream-wrappers.test.ts +++ b/src/agents/pi-embedded-runner/openai-stream-wrappers.test.ts @@ -159,6 +159,32 @@ describe("createOpenAIThinkingLevelWrapper", () => { } }); + it("raises minimal reasoning for web_search on loopback Responses routes", () => { + const payloads: Array> = []; + const baseStreamFn: StreamFn = (_model, _context, options) => { + const payload: Record = { + 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", diff --git a/src/agents/pi-embedded-runner/openai-stream-wrappers.ts b/src/agents/pi-embedded-runner/openai-stream-wrappers.ts index 679211f8b5c..95e12800482 100644 --- a/src/agents/pi-embedded-runner/openai-stream-wrappers.ts +++ b/src/agents/pi-embedded-runner/openai-stream-wrappers.ts @@ -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 { + 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; + 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; +}): 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).effort = - mapThinkingLevelToReasoningEffort(thinkingLevel); + (existingReasoning as Record).effort = reasoningEffort; + raiseMinimalReasoningForResponsesWebSearchPayload({ model, payloadObj }); } }); };