fix(providers): guard OpenAI web search tool detection

This commit is contained in:
Vincent Koc
2026-06-04 12:35:02 +02:00
parent 21031c2243
commit 85de1ab264
2 changed files with 96 additions and 4 deletions

View File

@@ -485,6 +485,80 @@ describe("createOpenAIThinkingLevelWrapper", () => {
expect(payloads[0]?.reasoning).toEqual({ effort: "low", summary: "auto" });
});
it("skips unreadable payload tools while detecting web_search reasoning needs", () => {
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",
get function(): never {
throw new Error("payload tool function getter exploded");
},
},
{ type: "function", name: "web_search" },
],
};
options?.onPayload?.(payload, model);
payloads.push({ reasoning: payload.reasoning });
return createAssistantMessageEventStream();
};
const wrapped = createOpenAIThinkingLevelWrapper(baseStreamFn, "minimal");
expect(
() =>
void wrapped(
{
api: "openai-responses",
provider: "openai",
id: "gpt-5",
baseUrl: "http://127.0.0.1:19191/v1",
} as Model<"openai-responses">,
{ messages: [] },
{},
),
).not.toThrow();
expect(payloads[0]?.reasoning).toEqual({ effort: "low", summary: "auto" });
});
it("detects nested web_search when the top-level payload tool name is unreadable", () => {
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",
get name(): never {
throw new Error("payload tool name getter exploded");
},
function: { name: "web_search" },
},
],
};
options?.onPayload?.(payload, model);
payloads.push({ reasoning: payload.reasoning });
return createAssistantMessageEventStream();
};
const wrapped = createOpenAIThinkingLevelWrapper(baseStreamFn, "minimal");
expect(
() =>
void wrapped(
{
api: "openai-responses",
provider: "openai",
id: "gpt-5",
baseUrl: "http://127.0.0.1:19191/v1",
} as Model<"openai-responses">,
{ messages: [] },
{},
),
).not.toThrow();
expect(payloads[0]?.reasoning).toEqual({ effort: "low", summary: "auto" });
});
it.each([
{
api: "openai-responses",

View File

@@ -40,6 +40,7 @@ type OpenClawSimpleStreamOptions = SimpleStreamOptions & {
type OpenAIResponsesReplayOptions = Parameters<StreamFn>[2] & {
replayResponsesItemIds?: boolean;
};
type PayloadFieldRead = { ok: true; value: unknown } | { ok: false };
export { resolveOpenAITextVerbosity };
function resolveOpenAITextVerbosityForModel(
@@ -236,6 +237,14 @@ function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value && typeof value === "object" && !Array.isArray(value));
}
function readPayloadField(record: Record<string, unknown>, field: string): PayloadFieldRead {
try {
return { ok: true, value: Reflect.get(record, field) };
} catch {
return { ok: false };
}
}
function hasResponsesWebSearchTool(tools: unknown): boolean {
if (!Array.isArray(tools)) {
return false;
@@ -244,14 +253,23 @@ function hasResponsesWebSearchTool(tools: unknown): boolean {
if (!isRecord(tool)) {
return false;
}
if (tool.type === "web_search") {
const type = readPayloadField(tool, "type");
if (!type.ok) {
return false;
}
if (type.value === "web_search") {
return true;
}
if (tool.type === "function" && tool.name === "web_search") {
const name = readPayloadField(tool, "name");
if (name.ok && type.value === "function" && name.value === "web_search") {
return true;
}
const fn = tool.function;
return isRecord(fn) && fn.name === "web_search";
const fn = readPayloadField(tool, "function");
if (!fn.ok || !isRecord(fn.value)) {
return false;
}
const functionName = readPayloadField(fn.value, "name");
return functionName.ok && functionName.value === "web_search";
});
}