Skip to content

Commit 386d2e3

Browse files
committed
fix(openai): guard responses tool payload names
1 parent 6b25b78 commit 386d2e3

2 files changed

Lines changed: 79 additions & 15 deletions

File tree

src/agents/openai-transport-stream.test.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,32 @@ describe("openai transport stream", () => {
480480
}
481481
});
482482

483+
it("skips unreadable model payload tool names in debug summaries", () => {
484+
const previous = process.env.OPENCLAW_DEBUG_MODEL_PAYLOAD;
485+
process.env.OPENCLAW_DEBUG_MODEL_PAYLOAD = "tools";
486+
try {
487+
const hostileTool = {
488+
type: "function",
489+
get function(): { name: string } {
490+
throw new Error("responses debug tool function getter exploded");
491+
},
492+
};
493+
494+
expect(
495+
testing.summarizeResponsesTools([
496+
hostileTool,
497+
{ type: "function", function: { name: "wait" } },
498+
]),
499+
).toBe("count=2 names=wait");
500+
} finally {
501+
if (previous === undefined) {
502+
delete process.env.OPENCLAW_DEBUG_MODEL_PAYLOAD;
503+
} else {
504+
process.env.OPENCLAW_DEBUG_MODEL_PAYLOAD = previous;
505+
}
506+
}
507+
});
508+
483509
it("redacts full model payload debug summaries", () => {
484510
const previous = process.env.OPENCLAW_DEBUG_MODEL_PAYLOAD;
485511
process.env.OPENCLAW_DEBUG_MODEL_PAYLOAD = "full-redacted";
@@ -517,6 +543,25 @@ describe("openai transport stream", () => {
517543
expect(payload.tools).toHaveLength(2);
518544
});
519545

546+
it("skips unreadable code mode response payload tool names", () => {
547+
const payload = {
548+
tools: [
549+
{ type: "function", name: "exec" },
550+
{
551+
type: "function",
552+
get function(): { name: string } {
553+
throw new Error("responses code mode function getter exploded");
554+
},
555+
},
556+
{ type: "function", function: { name: "wait" } },
557+
],
558+
};
559+
560+
testing.enforceCodeModeResponsesToolSurface(payload);
561+
testing.assertCodeModeResponsesToolSurface(payload);
562+
expect(payload.tools).toHaveLength(2);
563+
});
564+
520565
it("fails closed when the code mode final payload tool surface is not exec/wait", () => {
521566
expect(() =>
522567
testing.assertCodeModeResponsesToolSurface({

src/agents/openai-transport-stream.ts

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -342,19 +342,32 @@ function responseInputRoles(input: unknown): string {
342342
return [...roles].toSorted().join(",");
343343
}
344344

345+
function readToolPayloadField(record: Record<string, unknown>, field: string): unknown {
346+
try {
347+
return record[field];
348+
} catch {
349+
return undefined;
350+
}
351+
}
352+
345353
function readResponsesToolDisplayName(tool: unknown): string {
346354
if (!tool || typeof tool !== "object") {
347355
return "";
348356
}
349357
const record = tool as Record<string, unknown>;
350-
if (typeof record.name === "string") {
351-
return record.name;
352-
}
353-
const fn = record.function;
354-
if (fn && typeof fn === "object" && typeof (fn as Record<string, unknown>).name === "string") {
355-
return (fn as Record<string, unknown>).name as string;
358+
const name = readToolPayloadField(record, "name");
359+
if (typeof name === "string") {
360+
return name;
361+
}
362+
const fn = readToolPayloadField(record, "function");
363+
if (fn && typeof fn === "object") {
364+
const fnName = readToolPayloadField(fn as Record<string, unknown>, "name");
365+
if (typeof fnName === "string") {
366+
return fnName;
367+
}
356368
}
357-
return typeof record.type === "string" ? record.type : "";
369+
const type = readToolPayloadField(record, "type");
370+
return typeof type === "string" && type !== "function" ? type : "";
358371
}
359372

360373
function summarizeResponsesTools(tools: unknown): string {
@@ -373,11 +386,16 @@ function responsesPayloadToolName(tool: unknown): string | undefined {
373386
if (!isRecord(tool)) {
374387
return undefined;
375388
}
376-
if (typeof tool.name === "string") {
377-
return tool.name;
389+
const name = readToolPayloadField(tool, "name");
390+
if (typeof name === "string") {
391+
return name;
392+
}
393+
const fn = readToolPayloadField(tool, "function");
394+
if (!isRecord(fn)) {
395+
return undefined;
378396
}
379-
const fn = tool.function;
380-
return isRecord(fn) && typeof fn.name === "string" ? fn.name : undefined;
397+
const fnName = readToolPayloadField(fn, "name");
398+
return typeof fnName === "string" ? fnName : undefined;
381399
}
382400

383401
function enforceCodeModeResponsesToolSurface(payload: unknown): void {
@@ -2026,14 +2044,15 @@ function hasResponsesWebSearchTool(tools: unknown): boolean {
20262044
if (!isRecord(tool)) {
20272045
return false;
20282046
}
2029-
if (tool.type === "web_search") {
2047+
const type = readToolPayloadField(tool, "type");
2048+
if (type === "web_search") {
20302049
return true;
20312050
}
2032-
if (tool.type === "function" && tool.name === "web_search") {
2051+
if (type === "function" && readToolPayloadField(tool, "name") === "web_search") {
20332052
return true;
20342053
}
2035-
const fn = tool.function;
2036-
return isRecord(fn) && fn.name === "web_search";
2054+
const fn = readToolPayloadField(tool, "function");
2055+
return isRecord(fn) && readToolPayloadField(fn, "name") === "web_search";
20372056
});
20382057
}
20392058

0 commit comments

Comments
 (0)