mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:50:43 +00:00
fix(agents): keep responses web search reasoning compatible
This commit is contained in:
@@ -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) => {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user